Compare commits
21 Commits
media-serv
...
bce43c4a71
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bce43c4a71 | ||
|
|
60e3a41ac5 | ||
|
|
b9301fff36 | ||
|
|
80f8857dd2 | ||
|
|
2c8c43ff68 | ||
|
|
4edfd642f3 | ||
|
|
c59ebbcc8b | ||
|
|
8ee71193bb | ||
|
|
4f5ee19cef | ||
|
|
9c46aa0d5b | ||
|
|
0a076a9af5 | ||
|
|
6595de4788 | ||
|
|
3725c73bce | ||
|
|
eefe369975 | ||
|
|
ede6c52a40 | ||
|
|
a63f144bf1 | ||
|
|
82d8ca0463 | ||
|
|
c02ac6f961 | ||
|
|
b03034b742 | ||
|
|
26808e4ca6 | ||
|
|
5cd8d36d97 |
6
main.tf
6
main.tf
@@ -19,6 +19,10 @@ module "services" {
|
||||
source = "./services"
|
||||
}
|
||||
|
||||
locals {
|
||||
volume_host = "${module.system_globals.volume_host}/appdata"
|
||||
}
|
||||
|
||||
module "homelab_cloudflared_tunnel" {
|
||||
source = "./modules/01-networking/cloudflared-tunnel"
|
||||
cloudflare_account_id = module.cloudflare_globals.cloudflare_account_id
|
||||
@@ -40,7 +44,7 @@ module "homelab_caddy_proxy" {
|
||||
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
|
||||
external_ip = module.cloudflare_globals.external_ip
|
||||
service_definitions = module.services.service_definitions
|
||||
volume_path = module.system_globals.volume_host
|
||||
volume_path = local.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
monitoring = true
|
||||
}
|
||||
|
||||
@@ -63,10 +63,6 @@ locals {
|
||||
])
|
||||
}
|
||||
|
||||
resource "docker_volume" "caddy_config" {
|
||||
name = "${local.container_name}_config"
|
||||
}
|
||||
|
||||
// Create Caddyfile in the volume path
|
||||
resource "local_file" "caddyfile" {
|
||||
content = local.caddyfile_content
|
||||
@@ -115,12 +111,12 @@ module "caddy" {
|
||||
|
||||
ports = [
|
||||
{
|
||||
external = "9080"
|
||||
external = "80"
|
||||
internal = "80"
|
||||
protocol = "tcp"
|
||||
},
|
||||
{
|
||||
external = "9443"
|
||||
external = "443"
|
||||
internal = "443"
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "this" {
|
||||
}
|
||||
|
||||
module "dns_records" {
|
||||
source = "../../10-services-generic/cloudflare-dns"
|
||||
source = "../../10-services-generic/cloudflare-dns"
|
||||
zone_id = var.cloudflare_zone_id
|
||||
hostnames = [
|
||||
for rule in local.all_ingress_rules :
|
||||
|
||||
@@ -71,7 +71,7 @@ resource "docker_container" "service_container" {
|
||||
|
||||
# Set the network mode (bridge, host, etc.)
|
||||
network_mode = local.network_mode
|
||||
|
||||
|
||||
# Add host mappings (entries for /etc/hosts)
|
||||
dynamic "host" {
|
||||
for_each = var.host_mappings
|
||||
@@ -139,15 +139,36 @@ resource "docker_container" "service_container" {
|
||||
cpu_shares = var.cpu_shares
|
||||
|
||||
# Other container options
|
||||
dns = var.dns
|
||||
dns_search = var.dns_search
|
||||
hostname = var.hostname
|
||||
domainname = var.domainname
|
||||
user = var.user
|
||||
working_dir = var.working_dir
|
||||
command = var.command
|
||||
entrypoint = var.entrypoint
|
||||
privileged = var.privileged
|
||||
dns = var.dns
|
||||
dns_search = var.dns_search
|
||||
hostname = var.hostname
|
||||
domainname = var.domainname
|
||||
user = var.user
|
||||
group_add = var.group_add
|
||||
working_dir = var.working_dir
|
||||
command = var.command
|
||||
entrypoint = var.entrypoint
|
||||
privileged = var.privileged
|
||||
destroy_grace_seconds = var.destroy_grace_seconds
|
||||
|
||||
# Linux capabilities controls
|
||||
dynamic "capabilities" {
|
||||
for_each = length(var.capabilities_add) > 0 || length(var.capabilities_drop) > 0 ? [1] : []
|
||||
content {
|
||||
add = var.capabilities_add
|
||||
drop = var.capabilities_drop
|
||||
}
|
||||
}
|
||||
|
||||
# Device mappings
|
||||
dynamic "devices" {
|
||||
for_each = var.devices
|
||||
content {
|
||||
host_path = devices.value.host_path
|
||||
container_path = devices.value.container_path
|
||||
permissions = devices.value.permissions
|
||||
}
|
||||
}
|
||||
|
||||
# Set log options
|
||||
log_driver = var.log_driver
|
||||
|
||||
@@ -179,12 +179,48 @@ variable "entrypoint" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group_add" {
|
||||
description = "Additional groups to add to the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "privileged" {
|
||||
description = "Run container in privileged mode"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
// Linux capabilities controls
|
||||
variable "capabilities_add" {
|
||||
description = "Linux capabilities to add to the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "capabilities_drop" {
|
||||
description = "Linux capabilities to drop from the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
// Devices to pass through to container
|
||||
variable "devices" {
|
||||
description = "List of device mappings for the container"
|
||||
type = list(object({
|
||||
host_path = string
|
||||
container_path = string
|
||||
permissions = string
|
||||
}))
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "destroy_grace_seconds" {
|
||||
description = "Grace period in seconds before the container is destroyed"
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
|
||||
// Logging options
|
||||
variable "log_driver" {
|
||||
description = "Log driver for the container"
|
||||
@@ -195,8 +231,8 @@ variable "log_driver" {
|
||||
variable "log_opts" {
|
||||
description = "Log driver options"
|
||||
type = map(string)
|
||||
default = {
|
||||
max-size = "10m"
|
||||
max-file = "3"
|
||||
default = {
|
||||
max-size = "10m"
|
||||
max-file = "3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ output "service_definition" {
|
||||
name = local.container_name
|
||||
primary_port = local.exposed_port
|
||||
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "tunnel"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,6 @@ output "service_definition" {
|
||||
endpoint = "http://${local.container_name}:${local.affine_internal_port}"
|
||||
subdomains = ["notes"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -42,13 +42,13 @@ variable "timezone" {
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "calibre-web-automated"
|
||||
calibre_image = "crocodilestick/calibre-web-automated"
|
||||
calibre_tag = var.image_tag
|
||||
monitoring = true
|
||||
env_file = "${path.module}/.env"
|
||||
calibre_internal_port = 8083
|
||||
docker_mods = "lscr.io/linuxserver/mods:universal-calibre-v7.16.0"
|
||||
container_name = "calibre-web-automated"
|
||||
calibre_image = "crocodilestick/calibre-web-automated"
|
||||
calibre_tag = var.image_tag
|
||||
monitoring = true
|
||||
env_file = "${path.module}/.env"
|
||||
calibre_internal_port = 8083
|
||||
docker_mods = "lscr.io/linuxserver/mods:universal-calibre-v7.16.0"
|
||||
|
||||
# Define volumes
|
||||
calibre_volumes = [
|
||||
@@ -99,6 +99,6 @@ output "service_definition" {
|
||||
endpoint = "http://${local.container_name}:${local.calibre_internal_port}"
|
||||
subdomains = ["calibre"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
|
||||
85
modules/20-services-apps/copyparty/README.md
Normal file
85
modules/20-services-apps/copyparty/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Copyparty Module
|
||||
|
||||
This module deploys [copyparty](https://github.com/9001/copyparty), a portable file server.
|
||||
|
||||
## Overview
|
||||
|
||||
The copyparty module:
|
||||
|
||||
- Deploys one Docker container: `copyparty`.
|
||||
- Mounts a volume for configuration and another for the files to be shared.
|
||||
- Provides a service definition for integration with networking modules.
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "copyparty" {
|
||||
source = "./modules/20-services-apps/copyparty"
|
||||
fileshare_path = "/path/to/your/fileshare/top/folder"
|
||||
config_path = "/path/to/copyparty/config"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------- | ----------------------------------------------------------- |
|
||||
| `image_tag` | Tag of the copyparty image to use |
|
||||
| `fileshare_path` | Host path for the top folder of the file share |
|
||||
| `config_path` | Host path for copyparty configuration files |
|
||||
| `networks` | List of additional networks to which copyparty should be attached |
|
||||
| `puid` | User ID to run the container as |
|
||||
| `pgid` | Group ID to run the container as |
|
||||
|
||||
## 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 = "copyparty"
|
||||
primary_port = 3923
|
||||
endpoint = "http://copyparty:3923"
|
||||
subdomains = ["files"]
|
||||
publish_via = "reverse_proxy"
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Copyparty uses two volumes:
|
||||
|
||||
1. Configuration: `/cfg` in the container, mapped to `var.config_path` on the host.
|
||||
2. File Share: `/w` in the container, mapped to `var.fileshare_path` on the host.
|
||||
|
||||
## 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
|
||||
module "copyparty" {
|
||||
source = "./modules/20-services-apps/copyparty"
|
||||
fileshare_path = "/mnt/storage/files"
|
||||
config_path = "${module.system_globals.volume_host}/copyparty/config"
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.copyparty.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
89
modules/20-services-apps/copyparty/main.tf
Normal file
89
modules/20-services-apps/copyparty/main.tf
Normal file
@@ -0,0 +1,89 @@
|
||||
variable "image_tag" {
|
||||
description = "The tag for the copyparty container image"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "fileshare_path" {
|
||||
description = "Path to the top folder of the file share"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "config_path" {
|
||||
description = "Path to the configuration files for copyparty"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "puid" {
|
||||
description = "User ID to run the container as"
|
||||
type = string
|
||||
default = "1000"
|
||||
}
|
||||
|
||||
variable "pgid" {
|
||||
description = "Group ID to run the container as"
|
||||
type = string
|
||||
default = "1000"
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "copyparty"
|
||||
image = "copyparty/ac"
|
||||
tag = var.image_tag
|
||||
monitoring = true
|
||||
internal_port = 3923
|
||||
user = "${var.puid}:${var.pgid}"
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.config_path
|
||||
container_path = "/cfg"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.fileshare_path
|
||||
container_path = "/w"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
env_vars = {
|
||||
LD_PRELOAD = "/usr/lib/libmimalloc-secure.so.2"
|
||||
PYTHONUNBUFFERED = "1"
|
||||
}
|
||||
}
|
||||
|
||||
module "copyparty" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
user = local.user
|
||||
volumes = local.volumes
|
||||
env_vars = local.env_vars
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
destroy_grace_seconds = 15
|
||||
ports = [
|
||||
{
|
||||
internal = local.internal_port
|
||||
external = local.internal_port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
subdomains = ["drive"]
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
18
modules/20-services-apps/crawl4ai/.env.example
Normal file
18
modules/20-services-apps/crawl4ai/.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
# Crawl4AI Configuration
|
||||
PORT=11235
|
||||
|
||||
# API Keys for LLM providers used by Crawl4AI
|
||||
# OpenAI API key for GPT models
|
||||
OPENAI_API_KEY=
|
||||
# DeepSeek API key
|
||||
DEEPSEEK_API_KEY=
|
||||
# Anthropic API key for Claude models
|
||||
ANTHROPIC_API_KEY=
|
||||
# Groq API key
|
||||
GROQ_API_KEY=
|
||||
# Together API key
|
||||
TOGETHER_API_KEY=
|
||||
# Mistral API key
|
||||
MISTRAL_API_KEY=
|
||||
# Google Gemini API token
|
||||
GEMINI_API_TOKEN=
|
||||
91
modules/20-services-apps/crawl4ai/README.md
Normal file
91
modules/20-services-apps/crawl4ai/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Crawl4AI Module
|
||||
|
||||
This module deploys [Crawl4AI](https://github.com/unclecode/crawl4ai), a web crawling and AI analysis tool, as a Docker container in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Crawl4AI module:
|
||||
|
||||
- Deploys the `unclecode/crawl4ai` Docker container
|
||||
- Configures resource limits and reservations for memory
|
||||
- Provides shared memory access for Chrome/Chromium performance
|
||||
- Supports custom configuration through volume mounting
|
||||
- Provides service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "crawl4ai" {
|
||||
source = "./modules/20-services-apps/crawl4ai"
|
||||
volume_path = "/path/to/volumes"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| --------------------- | ------------------------------------------------- | -------------- | ----------- |
|
||||
| `image_tag` | Tag of the Crawl4AI image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for Crawl4AI data volumes | `string` | - |
|
||||
| `networks` | List of networks to attach the container to | `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 = "crawl4ai"
|
||||
primary_port = 11235
|
||||
endpoint = "http://crawl4ai:11235"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Crawl4AI requires API keys for various LLM providers. These are configured through a `.env` file in the module directory. You should create this file based on the provided `.env.example` template:
|
||||
|
||||
- `OPENAI_API_KEY`: OpenAI API key
|
||||
- `DEEPSEEK_API_KEY`: DeepSeek API key
|
||||
- `ANTHROPIC_API_KEY`: Anthropic API key
|
||||
- `GROQ_API_KEY`: Groq API key
|
||||
- `TOGETHER_API_KEY`: Together API key
|
||||
- `MISTRAL_API_KEY`: Mistral API key
|
||||
- `GEMINI_API_TOKEN`: Gemini API token
|
||||
|
||||
## Configuration
|
||||
|
||||
Crawl4AI requires a custom configuration file. This is mounted from `${volume_path}/crawl4ai/config.yml` to `/app/config.yml` in the container.
|
||||
|
||||
## Ports
|
||||
|
||||
Crawl4AI exposes one port, which is mapped to host ports defined in the `.env` file:
|
||||
1. Frontend (port 11235) - The main web interface for accessing games
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "crawl4ai" {
|
||||
source = "./modules/20-services-apps/crawl4ai"
|
||||
volume_path = module.system_globals.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
memory_limit = 8192 # 8GB if you need more memory
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.crawl4ai.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
99
modules/20-services-apps/crawl4ai/main.tf
Normal file
99
modules/20-services-apps/crawl4ai/main.tf
Normal file
@@ -0,0 +1,99 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Tag of the Crawl4AI image to use"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Host path for Crawl4AI data volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "crawl4ai"
|
||||
image = "unclecode/crawl4ai"
|
||||
image_tag = var.image_tag
|
||||
monitoring = true
|
||||
service_port = provider::dotenv::get_by_key("PORT", local.env_file)
|
||||
env_file = "${path.module}/.env"
|
||||
|
||||
# Define volumes
|
||||
default_volumes = [
|
||||
{
|
||||
container_path = "/dev/shm"
|
||||
host_path = "/dev/shm"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
container_path = "/app/config.yml"
|
||||
host_path = "${var.volume_path}/config.yml"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
# Define ports
|
||||
ports = [
|
||||
{
|
||||
internal = local.service_port
|
||||
external = local.service_port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
|
||||
# Environment variables
|
||||
env_vars = {
|
||||
OPENAI_API_KEY = provider::dotenv::get_by_key("OPENAI_API_KEY", local.env_file)
|
||||
DEEPSEEK_API_KEY = provider::dotenv::get_by_key("DEEPSEEK_API_KEY", local.env_file)
|
||||
ANTHROPIC_API_KEY = provider::dotenv::get_by_key("ANTHROPIC_API_KEY", local.env_file)
|
||||
GROQ_API_KEY = provider::dotenv::get_by_key("GROQ_API_KEY", local.env_file)
|
||||
TOGETHER_API_KEY = provider::dotenv::get_by_key("TOGETHER_API_KEY", local.env_file)
|
||||
MISTRAL_API_KEY = provider::dotenv::get_by_key("MISTRAL_API_KEY", local.env_file)
|
||||
GEMINI_API_TOKEN = provider::dotenv::get_by_key("GEMINI_API_TOKEN", local.env_file)
|
||||
}
|
||||
|
||||
# Healthcheck configuration
|
||||
healthcheck = {
|
||||
test = ["CMD", "curl", "-f", "http://localhost:${local.service_port}/health"]
|
||||
interval = "30s"
|
||||
timeout = "10s"
|
||||
retries = 3
|
||||
start_period = "40s"
|
||||
}
|
||||
}
|
||||
|
||||
module "crawl4ai" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.image_tag
|
||||
volumes = local.default_volumes
|
||||
ports = local.ports
|
||||
env_vars = local.env_vars
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.healthcheck
|
||||
user = "appuser"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.service_port
|
||||
endpoint = "http://${local.container_name}:${local.service_port}"
|
||||
}
|
||||
}
|
||||
77
modules/20-services-apps/glance/README.md
Normal file
77
modules/20-services-apps/glance/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Glance Module
|
||||
|
||||
This module deploys [Glance](https://glanceapp.io/), a dashboard application, as a Docker container in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Glance module:
|
||||
|
||||
- Deploys the `glanceapp/glance` Docker container
|
||||
- Persists configuration to a volume on the host
|
||||
- Provides service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "glance" {
|
||||
source = "./modules/20-services-apps/glance"
|
||||
volume_path = "/path/to/volumes/glance"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the Glance image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for Glance data volume | `string` | - |
|
||||
| `networks` | List of networks to which the container should be attached | `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 = "glance"
|
||||
primary_port = 4921
|
||||
endpoint = "http://glance:4921"
|
||||
subdomains = ["glance"]
|
||||
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Glance stores its configuration in the `/app/config` directory inside the container. This is mapped to a volume on the host at `${volume_path}/config`.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "glance" {
|
||||
source = "./modules/20-services-apps/glance"
|
||||
volume_path = module.system_globals.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.glance.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
52
modules/20-services-apps/glance/main.tf
Normal file
52
modules/20-services-apps/glance/main.tf
Normal file
@@ -0,0 +1,52 @@
|
||||
variable "image_tag" {
|
||||
description = "Tag of the Glance image to use"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Host path for Glance data volume"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "glance"
|
||||
image = "glanceapp/glance"
|
||||
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
monitoring = true
|
||||
host_port = 8080
|
||||
subdomains = ["glance"]
|
||||
default_volumes = [
|
||||
{
|
||||
container_path = "/app/config"
|
||||
host_path = "${var.volume_path}/config"
|
||||
read_only = false
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
module "glance" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.image_tag
|
||||
volumes = local.default_volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.host_port
|
||||
endpoint = "http://${local.container_name}:${local.host_port}"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
14
modules/20-services-apps/immich/.env.example
Normal file
14
modules/20-services-apps/immich/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
|
||||
|
||||
###################################################################################
|
||||
# Required database configuration (used by Terraform to configure Postgres & Immich)
|
||||
###################################################################################
|
||||
|
||||
# PostgreSQL username
|
||||
DB_USERNAME=postgres
|
||||
|
||||
# PostgreSQL password (use only A-Za-z0-9 characters)
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# PostgreSQL database name
|
||||
DB_DATABASE_NAME=immich
|
||||
110
modules/20-services-apps/immich/README.md
Normal file
110
modules/20-services-apps/immich/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Immich Module
|
||||
|
||||
This module deploys [Immich](https://immich.app/), a high-performance self-hosted photo and video backup solution, as Docker containers in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Immich module:
|
||||
|
||||
- Deploys four Docker containers:
|
||||
- `immich-server`: The main Immich API/UI server (port 2283)
|
||||
- `immich-machine-learning`: The ML service for search, faces, and embeddings
|
||||
- `immich-postgres`: Immich-tuned PostgreSQL database
|
||||
- `immich-redis`: Valkey/Redis-compatible cache
|
||||
- Creates a dedicated Docker network (`immich-network`) for inter-container communication
|
||||
- Persists data to volumes on the host
|
||||
- Provides a service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "immich" {
|
||||
source = "./modules/20-services-apps/immich"
|
||||
appdata_path = "/path/to/appdata/immich"
|
||||
library_path = "/path/to/data/media/photos"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| --------------- | -------------------------------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the Immich images to use (`server` and `machine-learning`) | `string` | `"release"` |
|
||||
| `appdata_path` | Base host path for Immich app data (e.g., PostgreSQL data and internal configs) | `string` | - |
|
||||
| `library_path` | Base host path for user library data and ML cache | `string` | - |
|
||||
| `networks` | List of additional networks to which the server should attach | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition used by networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "immich-server"
|
||||
primary_port = 2283
|
||||
endpoint = "http://immich-server:2283"
|
||||
subdomains = ["photos"]
|
||||
publish_via = "reverse_proxy"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Only the database credentials are expected in a `.env` file in this module directory and are read using the `dotenv` Terraform provider. Everything else is configured directly in Terraform.
|
||||
|
||||
Required in `modules/20-services-apps/immich/.env`:
|
||||
|
||||
- `DB_USERNAME`: PostgreSQL user
|
||||
- `DB_PASSWORD`: PostgreSQL password
|
||||
- `DB_DATABASE_NAME`: Database name
|
||||
|
||||
A ready-to-copy `modules/20-services-apps/immich/.env.example` is provided.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Immich stores data in the following volumes:
|
||||
|
||||
1. Library storage: `/data` in `immich-server`, mapped to `${library_path}/library` on the host
|
||||
2. ML model cache: `/cache` in `immich-machine-learning`, mapped to `${library_path}/machine-learning/cache` on the host
|
||||
3. PostgreSQL data: `/var/lib/postgresql/data` in `immich-postgres`, mapped to `${appdata_path}/postgres/pgdata` on the host
|
||||
|
||||
## Networking
|
||||
|
||||
The module creates a dedicated Docker network named `immich-network` for communication between Immich components. The Immich server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `immich-server` depends on `immich-postgres` and `immich-redis`
|
||||
- `immich-postgres` and `immich-redis` include healthchecks
|
||||
- The ML service is independent and discovered by the server internally; tuning can be done via the Immich admin UI
|
||||
|
||||
## 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
|
||||
module "immich" {
|
||||
source = "./modules/20-services-apps/immich"
|
||||
appdata_path = "${module.system_globals.volume_host}/appdata/immich"
|
||||
library_path = "${module.system_globals.volume_host}/data/media/photos"
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.immich.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
197
modules/20-services-apps/immich/main.tf
Normal file
197
modules/20-services-apps/immich/main.tf
Normal file
@@ -0,0 +1,197 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "The tag for the Immich container images (server and machine-learning)"
|
||||
type = string
|
||||
default = "release"
|
||||
}
|
||||
|
||||
variable "appdata_path" {
|
||||
description = "Base directory for Immich app data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "library_path" {
|
||||
description = "Base directory for Immich library data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the Immich server should be attached (in addition to the module network)"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
monitoring = true
|
||||
|
||||
# Container names
|
||||
server_name = "immich-server"
|
||||
ml_name = "immich-machine-learning"
|
||||
redis_name = "immich-redis"
|
||||
postgres_name = "immich-postgres"
|
||||
|
||||
# Images and tags
|
||||
server_image = "ghcr.io/immich-app/immich-server"
|
||||
ml_image = "ghcr.io/immich-app/immich-machine-learning"
|
||||
redis_image = "docker.io/valkey/valkey"
|
||||
postgres_image = "ghcr.io/immich-app/postgres"
|
||||
|
||||
server_tag = var.image_tag
|
||||
ml_tag = var.image_tag
|
||||
redis_tag = "8-bookworm"
|
||||
postgres_tag = "14-vectorchord0.4.3-pgvectors0.2.0"
|
||||
|
||||
# Ports
|
||||
server_port = 2283
|
||||
ml_port = 3003
|
||||
|
||||
# Volumes (host paths)
|
||||
server_volumes = [
|
||||
{
|
||||
host_path = "${var.library_path}/data"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
ml_volumes = [
|
||||
{
|
||||
host_path = "${var.library_path}/ml/cache"
|
||||
container_path = "/cache"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
postgres_volumes = [
|
||||
{
|
||||
host_path = "${var.appdata_path}/postgres/pgdata"
|
||||
container_path = "/var/lib/postgresql/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
# Environment variables for Postgres
|
||||
postgres_env_vars = {
|
||||
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
|
||||
POSTGRES_INITDB_ARGS = "--data-checksums"
|
||||
}
|
||||
|
||||
# Environment variables for Immich server
|
||||
server_env_vars = {
|
||||
# Database
|
||||
DB_HOSTNAME = local.postgres_name
|
||||
DB_PORT = "5432"
|
||||
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||
DB_DATABASE_NAME = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
|
||||
|
||||
# Redis
|
||||
REDIS_HOSTNAME = local.redis_name
|
||||
REDIS_PORT = "6379"
|
||||
REDIS_DBINDEX = "0"
|
||||
|
||||
# General
|
||||
IMMICH_MEDIA_LOCATION = "/data"
|
||||
}
|
||||
|
||||
# Healthchecks
|
||||
redis_healthcheck = {
|
||||
test = ["CMD", "redis-cli", "ping"]
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
retries = 5
|
||||
start_period = "5s"
|
||||
}
|
||||
|
||||
postgres_healthcheck = {
|
||||
test = ["CMD", "pg_isready", "-U", provider::dotenv::get_by_key("DB_USERNAME", local.env_file), "-d", provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)]
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
retries = 5
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
# Dedicated network for Immich
|
||||
module "immich_network" {
|
||||
source = "../../01-networking/docker-network"
|
||||
name = "immich-network"
|
||||
driver = "bridge"
|
||||
}
|
||||
|
||||
# Valkey (Redis) service
|
||||
module "redis" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.redis_name
|
||||
image = local.redis_image
|
||||
tag = local.redis_tag
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.redis_healthcheck
|
||||
}
|
||||
|
||||
# Postgres service (Immich custom image)
|
||||
module "postgres" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.postgres_name
|
||||
image = local.postgres_image
|
||||
tag = local.postgres_tag
|
||||
volumes = local.postgres_volumes
|
||||
env_vars = local.postgres_env_vars
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.postgres_healthcheck
|
||||
}
|
||||
|
||||
# Immich Machine Learning service
|
||||
module "machine_learning" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.ml_name
|
||||
image = local.ml_image
|
||||
tag = local.ml_tag
|
||||
volumes = local.ml_volumes
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
# Immich Server service
|
||||
module "immich" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.server_name
|
||||
image = local.server_image
|
||||
tag = local.server_tag
|
||||
ports = [
|
||||
{
|
||||
internal = local.server_port
|
||||
external = local.server_port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
volumes = local.server_volumes
|
||||
env_vars = local.server_env_vars
|
||||
networks = concat([module.immich_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
depends_on = [module.postgres, module.redis]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.server_name
|
||||
primary_port = local.server_port
|
||||
endpoint = "http://${local.server_name}:${local.server_port}"
|
||||
subdomains = ["photos"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,7 @@ N8N_PORT=5678
|
||||
N8N_PROTOCOL=http
|
||||
WEBHOOK_URL=https://n8n.yourdomain.com/
|
||||
NODE_FUNCTION_ALLOW_EXTERNAL=*
|
||||
|
||||
# MCP
|
||||
N8N_MCP_AUTH_TOKEN=
|
||||
N8N_API_KEY=
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# n8n Module
|
||||
|
||||
This module deploys [n8n](https://n8n.io/), a workflow automation tool for technical people, as Docker containers in the homelab environment.
|
||||
This module deploys [n8n](https://n8n.io/), a workflow automation tool, along with its dependencies and the [n8n-mcp](https://github.com/czlonkowski/n8n-mcp) community node manager, as Docker containers in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The n8n module:
|
||||
|
||||
- Deploys two Docker containers:
|
||||
- Deploys four Docker containers:
|
||||
- `n8n`: The main workflow automation server
|
||||
- `n8n-postgres`: A PostgreSQL database backend
|
||||
- `n8n-redis`: A Redis instance for queuing
|
||||
- `n8n-mcp`: A community node management tool for n8n
|
||||
- Creates a dedicated Docker network (`n8n-network`) for container communication
|
||||
- Persists data to volumes on the host
|
||||
- Provides service definition for integration with networking modules
|
||||
- Provides service definitions for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -28,18 +30,21 @@ module "n8n" {
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the n8n image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for n8n and Postgres data volumes | `string` | - |
|
||||
| `volume_path` | Host path for n8n, Postgres, Redis, and n8n-mcp data volumes | `string` | - |
|
||||
| `networks` | List of additional networks to which n8n should be attached | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
| Output | Description |
|
||||
| ---------------------------- | ---------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for the n8n container |
|
||||
| `n8n_mcp_service_definition` | Service definition for the n8n-mcp container |
|
||||
|
||||
## Service Definition
|
||||
## Service Definitions
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
This module outputs two service definitions that are used by the networking modules to expose the services.
|
||||
|
||||
### n8n
|
||||
|
||||
```hcl
|
||||
{
|
||||
@@ -47,45 +52,63 @@ This module outputs a service definition that is used by the networking modules
|
||||
primary_port = 5678
|
||||
endpoint = "http://n8n:5678"
|
||||
subdomains = ["n8n"]
|
||||
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
```
|
||||
|
||||
### n8n-mcp
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "n8n-mcp"
|
||||
primary_port = 3000
|
||||
endpoint = "http://n8n-mcp:3000"
|
||||
subdomains = ["n8n-mcp"]
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
n8n requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
|
||||
The services require several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
|
||||
|
||||
- Database configuration:
|
||||
- **Database configuration (`n8n-postgres`)**:
|
||||
- `POSTGRES_USER`: Root PostgreSQL user
|
||||
- `POSTGRES_PASSWORD`: Root PostgreSQL password
|
||||
- `POSTGRES_DB`: Database name for n8n
|
||||
- `POSTGRES_NON_ROOT_USER`: Non-root user for n8n to connect with
|
||||
- `POSTGRES_NON_ROOT_PASSWORD`: Password for the non-root user
|
||||
|
||||
- n8n configuration:
|
||||
- **n8n configuration (`n8n`)**:
|
||||
- `N8N_HOST`: Host for n8n to use
|
||||
- `N8N_PORT`: Port for n8n to use
|
||||
- `N8N_PROTOCOL`: Protocol for n8n (http or https)
|
||||
- `WEBHOOK_URL`: URL for webhooks
|
||||
- `NODE_FUNCTION_ALLOW_EXTERNAL`: Whether to allow external function calls
|
||||
|
||||
- **n8n-mcp configuration (`n8n-mcp`)**:
|
||||
- `N8N_MCP_AUTH_TOKEN`: Authentication token for n8n-mcp.
|
||||
- `N8N_API_KEY`: n8n API key for n8n-mcp to interact with the n8n instance.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
n8n stores its data in two main volumes:
|
||||
The services store data in several volumes:
|
||||
|
||||
1. n8n application data: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
|
||||
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
|
||||
1. **n8n application data**: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
|
||||
2. **PostgreSQL data**: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
|
||||
3. **Redis data**: `/data` in the container, mapped to `${volume_path}/redis_data` on the host
|
||||
4. **n8n-mcp data**: `/app/data` in the container, mapped to `${volume_path}/n8n_mcp_storage/_data` on the host
|
||||
|
||||
Additionally, an initialization script is mounted to the PostgreSQL container:
|
||||
- `/docker-entrypoint-initdb.d/init-data.sh` in the container, from `${volume_path}/init-data.sh` on the host
|
||||
|
||||
## Networking
|
||||
|
||||
The module creates a dedicated Docker network named `n8n-network` for communication between the n8n and PostgreSQL containers. The n8n container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||
The module creates a dedicated Docker network named `n8n-network` for communication between all containers. The `n8n` and `n8n-mcp` containers are also attached to any additional networks specified in the `networks` variable, allowing them to communicate with other services in the homelab.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||
The services are configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
@@ -96,13 +119,41 @@ module "n8n" {
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
# The service definitions are automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.n8n.service_definition,
|
||||
module.n8n.n8n_mcp_service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Using n8n-mcp with your IDE
|
||||
|
||||
To connect your IDE to the `n8n-mcp` server, you can use the following configuration in your IDE's settings. This allows the IDE to use the n8n instance as a tool provider.
|
||||
|
||||
Make sure to replace `<domain>` with your actual domain and populate the `AUTH_TOKEN` with the value of `N8N_MCP_AUTH_TOKEN` from your `.env` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"n8n-mcp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://n8n-mcp.<domain>/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer ${AUTH_TOKEN}",
|
||||
"--transport",
|
||||
"http-only"
|
||||
],
|
||||
"env": {
|
||||
"AUTH_TOKEN": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,17 +23,24 @@ variable "networks" {
|
||||
default = []
|
||||
}
|
||||
|
||||
module "system_globals" {
|
||||
source = "../../00-globals/system"
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "n8n"
|
||||
database_name = "n8n-postgres"
|
||||
n8n_image = "docker.n8n.io/n8nio/n8n"
|
||||
database_image = "postgres"
|
||||
n8n_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
database_tag = "16"
|
||||
monitoring = true
|
||||
env_file = "${path.module}/.env"
|
||||
container_name = "n8n"
|
||||
database_name = "n8n-postgres"
|
||||
redis_name = "n8n-redis"
|
||||
n8n_image = "docker.n8n.io/n8nio/n8n"
|
||||
database_image = "postgres"
|
||||
redis_image = "redis"
|
||||
n8n_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
database_tag = "16"
|
||||
redis_tag = "7-alpine"
|
||||
monitoring = true
|
||||
env_file = "${path.module}/.env"
|
||||
n8n_internal_port = 5678
|
||||
|
||||
|
||||
# Define volumes
|
||||
n8n_volumes = [
|
||||
{
|
||||
@@ -42,7 +49,7 @@ locals {
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
database_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/db_storage/_data"
|
||||
@@ -55,29 +62,34 @@ locals {
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# Environment variables for the database
|
||||
database_env_vars = {
|
||||
POSTGRES_USER = provider::dotenv::get_by_key("POSTGRES_USER", local.env_file)
|
||||
POSTGRES_PASSWORD = provider::dotenv::get_by_key("POSTGRES_PASSWORD", local.env_file)
|
||||
POSTGRES_DB = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
|
||||
POSTGRES_NON_ROOT_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
|
||||
POSTGRES_USER = provider::dotenv::get_by_key("POSTGRES_USER", local.env_file)
|
||||
POSTGRES_PASSWORD = provider::dotenv::get_by_key("POSTGRES_PASSWORD", local.env_file)
|
||||
POSTGRES_DB = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
|
||||
POSTGRES_NON_ROOT_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
|
||||
POSTGRES_NON_ROOT_PASSWORD = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_PASSWORD", local.env_file)
|
||||
}
|
||||
|
||||
|
||||
# Environment variables for n8n
|
||||
n8n_env_vars = {
|
||||
DB_TYPE = "postgresdb"
|
||||
DB_POSTGRESDB_HOST = local.database_name
|
||||
DB_POSTGRESDB_PORT = 5432
|
||||
DB_POSTGRESDB_DATABASE = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
|
||||
DB_POSTGRESDB_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
|
||||
DB_POSTGRESDB_PASSWORD = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_PASSWORD", local.env_file)
|
||||
N8N_HOST = provider::dotenv::get_by_key("N8N_HOST", local.env_file)
|
||||
N8N_PORT = provider::dotenv::get_by_key("N8N_PORT", local.env_file)
|
||||
N8N_PROTOCOL = provider::dotenv::get_by_key("N8N_PROTOCOL", local.env_file)
|
||||
WEBHOOK_URL = provider::dotenv::get_by_key("WEBHOOK_URL", local.env_file)
|
||||
NODE_FUNCTION_ALLOW_EXTERNAL = provider::dotenv::get_by_key("NODE_FUNCTION_ALLOW_EXTERNAL", local.env_file)
|
||||
DB_TYPE = "postgresdb"
|
||||
DB_POSTGRESDB_HOST = local.database_name
|
||||
DB_POSTGRESDB_PORT = 5432
|
||||
DB_POSTGRESDB_DATABASE = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
|
||||
DB_POSTGRESDB_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
|
||||
DB_POSTGRESDB_PASSWORD = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_PASSWORD", local.env_file)
|
||||
N8N_HOST = provider::dotenv::get_by_key("N8N_HOST", local.env_file)
|
||||
N8N_PORT = provider::dotenv::get_by_key("N8N_PORT", local.env_file)
|
||||
N8N_PROTOCOL = provider::dotenv::get_by_key("N8N_PROTOCOL", local.env_file)
|
||||
WEBHOOK_URL = provider::dotenv::get_by_key("WEBHOOK_URL", local.env_file)
|
||||
NODE_FUNCTION_ALLOW_EXTERNAL = provider::dotenv::get_by_key("NODE_FUNCTION_ALLOW_EXTERNAL", local.env_file)
|
||||
GENERIC_TIMEZONE = module.system_globals.timezone
|
||||
QUEUE_BULL_REDIS_HOST = local.redis_name
|
||||
QUEUE_BULL_REDIS_PORT = 6379
|
||||
QUEUE_BULL_REDIS_USERNAME = "redis"
|
||||
QUEUE_BULL_REDIS_PASSWORD = "redis"
|
||||
}
|
||||
|
||||
# Healthcheck configuration for the database
|
||||
@@ -88,12 +100,66 @@ locals {
|
||||
retries = 10
|
||||
start_period = "10s"
|
||||
}
|
||||
|
||||
# Healthcheck configuration for Redis
|
||||
redis_healthcheck = {
|
||||
test = ["CMD-SHELL", "redis-cli ping"]
|
||||
interval = "5s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "10s"
|
||||
}
|
||||
|
||||
# Define Redis volume
|
||||
redis_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/redis_data"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
n8n_mcp_container_name = "n8n-mcp"
|
||||
n8n_mcp_image = "ghcr.io/czlonkowski/n8n-mcp"
|
||||
n8n_mcp_tag = "latest"
|
||||
n8n_mcp_internal_port = 3000
|
||||
|
||||
n8n_mcp_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/n8n_mcp_storage/_data"
|
||||
container_path = "/app/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
n8n_mcp_env_vars = {
|
||||
MCP_MODE = "http"
|
||||
USE_FIXED_HTTP = "true"
|
||||
AUTH_TOKEN = provider::dotenv::get_by_key("N8N_MCP_AUTH_TOKEN", local.env_file)
|
||||
N8N_API_URL = "http://${local.container_name}:${local.n8n_internal_port}"
|
||||
N8N_API_KEY = provider::dotenv::get_by_key("N8N_API_KEY", local.env_file)
|
||||
NODE_ENV = "production"
|
||||
LOG_LEVEL = "info"
|
||||
PORT = local.n8n_mcp_internal_port
|
||||
NODE_DB_PATH = "/app/data/nodes.db"
|
||||
REBUILD_ON_START = "false"
|
||||
GENERIC_TIMEZONE = module.system_globals.timezone
|
||||
}
|
||||
|
||||
n8n_mcp_healthcheck = {
|
||||
test = ["CMD", "curl", "-f", "http://127.0.0.1:${local.n8n_mcp_internal_port}/health"]
|
||||
interval = "30s"
|
||||
timeout = "10s"
|
||||
retries = 3
|
||||
start_period = "40s"
|
||||
}
|
||||
}
|
||||
|
||||
module "n8n_network" {
|
||||
source = "../../01-networking/docker-network"
|
||||
name = "n8n-network"
|
||||
driver = "bridge"
|
||||
subnet = "172.24.0.0/16"
|
||||
}
|
||||
|
||||
# Create the PostgreSQL container
|
||||
@@ -103,12 +169,31 @@ module "postgres" {
|
||||
image = local.database_image
|
||||
tag = local.database_tag
|
||||
volumes = local.database_volumes
|
||||
user = "1000:1000"
|
||||
env_vars = local.database_env_vars
|
||||
networks = [module.n8n_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.database_healthcheck
|
||||
}
|
||||
|
||||
# Create the Redis container
|
||||
module "redis" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.redis_name
|
||||
image = local.redis_image
|
||||
tag = local.redis_tag
|
||||
volumes = local.redis_volumes
|
||||
user = "1000:1000"
|
||||
env_vars = {
|
||||
REDIS_USERNAME = "redis"
|
||||
REDIS_PASSWORD = "redis"
|
||||
}
|
||||
networks = [module.n8n_network.name]
|
||||
monitoring = local.monitoring
|
||||
command = ["redis-server", "--requirepass", "redis", "--user", "redis", "on", ">redis", "~*", "+@all"]
|
||||
healthcheck = local.redis_healthcheck
|
||||
}
|
||||
|
||||
# Create the n8n container
|
||||
module "n8n" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
@@ -116,10 +201,26 @@ module "n8n" {
|
||||
image = local.n8n_image
|
||||
tag = local.n8n_tag
|
||||
volumes = local.n8n_volumes
|
||||
user = "1000:1000"
|
||||
env_vars = local.n8n_env_vars
|
||||
networks = concat([module.n8n_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
depends_on = [module.postgres]
|
||||
depends_on = [module.postgres, module.redis]
|
||||
}
|
||||
|
||||
# Create the n8n-mcp container
|
||||
module "n8n_mcp" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.n8n_mcp_container_name
|
||||
image = local.n8n_mcp_image
|
||||
tag = local.n8n_mcp_tag
|
||||
volumes = local.n8n_mcp_volumes
|
||||
user = "1000:1000"
|
||||
env_vars = local.n8n_mcp_env_vars
|
||||
networks = concat([module.n8n_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.n8n_mcp_healthcheck
|
||||
depends_on = [module.n8n]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
@@ -132,3 +233,14 @@ output "service_definition" {
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
|
||||
output "n8n_mcp_service_definition" {
|
||||
description = "General service definition with optional ingress configuration for n8n-mcp"
|
||||
value = {
|
||||
name = local.n8n_mcp_container_name
|
||||
primary_port = local.n8n_mcp_internal_port
|
||||
endpoint = "http://${local.n8n_mcp_container_name}:${local.n8n_mcp_internal_port}"
|
||||
subdomains = ["n8n-mcp"]
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
|
||||
4
modules/20-services-apps/nocodb/.env.example
Normal file
4
modules/20-services-apps/nocodb/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Database Configuration
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=change_this_password
|
||||
DB_DATABASE=root_db
|
||||
106
modules/20-services-apps/nocodb/README.md
Normal file
106
modules/20-services-apps/nocodb/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# NocoDB Module
|
||||
|
||||
This module deploys [NocoDB](https://www.nocodb.com/), an open-source no-code database platform that transforms PostgreSQL into a smart spreadsheet interface, as Docker containers in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The NocoDB module:
|
||||
|
||||
- Deploys two Docker containers:
|
||||
- `nocodb`: The main NocoDB application server
|
||||
- `nocodb-postgres`: A PostgreSQL database backend
|
||||
- Creates a dedicated Docker network (`nocodb-network`) for container communication
|
||||
- Persists data to volumes on the host
|
||||
- Provides service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "nocodb" {
|
||||
source = "./modules/20-services-apps/nocodb"
|
||||
volume_path = "/path/to/volumes"
|
||||
networks = ["homelab-network"]
|
||||
postgres_user = "postgres"
|
||||
postgres_password = "your_secure_password"
|
||||
postgres_db = "root_db"
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------------- | -------------------------------------------------------------- | -------------- | ---------------------- |
|
||||
| `image_tag` | Tag of the NocoDB image to use | `string` | `"latest"` |
|
||||
| `postgres_image_tag`| Tag of the PostgreSQL image to use | `string` | `"16.6"` |
|
||||
| `volume_path` | Host path for NocoDB and database data volumes | `string` | - |
|
||||
| `networks` | List of networks to which NocoDB should be attached | `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 = "nocodb"
|
||||
primary_port = 8080
|
||||
endpoint = "http://nocodb:8080"
|
||||
subdomains = ["nocodb"]
|
||||
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
NocoDB requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
|
||||
|
||||
- Database configuration:
|
||||
|
||||
- `DB_USERNAME`: PostgreSQL user
|
||||
- `DB_PASSWORD`: PostgreSQL password
|
||||
- `DB_DATABASE`: Database name (defaults to "root_db")
|
||||
|
||||
## Data Persistence
|
||||
|
||||
NocoDB stores its data in two main volumes:
|
||||
|
||||
1. NocoDB application data: `/usr/app/data` in the container, mapped to `${volume_path}/nocodb/data` on the host
|
||||
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/nocodb/postgres/data` on the host
|
||||
|
||||
## Networking
|
||||
|
||||
The module creates a dedicated Docker network named `nocodb-network` for communication between the NocoDB components. The NocoDB server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The NocoDB container depends on PostgreSQL, which includes a healthcheck to ensure it's ready before NocoDB starts.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "nocodb" {
|
||||
source = "./modules/20-services-apps/nocodb"
|
||||
volume_path = module.system_globals.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
postgres_password = "your_secure_password"
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.nocodb.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
129
modules/20-services-apps/nocodb/main.tf
Normal file
129
modules/20-services-apps/nocodb/main.tf
Normal file
@@ -0,0 +1,129 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Tag of the NocoDB image to use"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "postgres_image_tag" {
|
||||
description = "Tag of the Postgres image to use"
|
||||
type = string
|
||||
default = "16.6"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Host path for NocoDB data volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the containers should be attached"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "nocodb"
|
||||
postgres_name = "nocodb-postgres"
|
||||
nocodb_image = "nocodb/nocodb"
|
||||
postgres_image = "postgres"
|
||||
nocodb_tag = var.image_tag
|
||||
postgres_tag = var.postgres_image_tag
|
||||
monitoring = true
|
||||
nocodb_internal_port = 8080
|
||||
env_file = "${path.module}/.env"
|
||||
postgres_user = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||
postgres_password = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||
postgres_db = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
|
||||
|
||||
# Define volumes
|
||||
nocodb_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/data"
|
||||
container_path = "/usr/app/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
postgres_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/postgres/data"
|
||||
container_path = "/var/lib/postgresql/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
# Environment variables for postgres
|
||||
postgres_env_vars = {
|
||||
POSTGRES_USER = local.postgres_user
|
||||
POSTGRES_PASSWORD = local.postgres_password
|
||||
POSTGRES_DB = local.postgres_db
|
||||
POSTGRES_INITDB_ARGS = "--data-checksums"
|
||||
POSTGRES_HOST_AUTH_METHOD = "trust"
|
||||
}
|
||||
|
||||
# Environment variables for NocoDB
|
||||
nocodb_env_vars = {
|
||||
NC_DB = "pg://${local.postgres_name}:5432?u=${local.postgres_user}&p=${local.postgres_password}&d=${local.postgres_db}"
|
||||
}
|
||||
|
||||
# Healthcheck configuration for Postgres
|
||||
postgres_healthcheck = {
|
||||
test = ["CMD", "pg_isready", "-U", local.postgres_user, "-d", local.postgres_db]
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "nocodb_network" {
|
||||
source = "../../01-networking/docker-network"
|
||||
name = "nocodb-network"
|
||||
subnet = "11.101.0.0/16"
|
||||
driver = "bridge"
|
||||
}
|
||||
|
||||
# Create the PostgreSQL container
|
||||
module "postgres" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.postgres_name
|
||||
image = local.postgres_image
|
||||
tag = local.postgres_tag
|
||||
volumes = local.postgres_volumes
|
||||
env_vars = local.postgres_env_vars
|
||||
networks = [module.nocodb_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.postgres_healthcheck
|
||||
}
|
||||
|
||||
# Create the NocoDB container
|
||||
module "nocodb" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.nocodb_image
|
||||
tag = local.nocodb_tag
|
||||
volumes = local.nocodb_volumes
|
||||
env_vars = local.nocodb_env_vars
|
||||
networks = concat([module.nocodb_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
depends_on = [module.postgres]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.nocodb_internal_port
|
||||
endpoint = "http://${local.container_name}:${local.nocodb_internal_port}"
|
||||
subdomains = ["db"]
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
78
modules/20-services-apps/portainer/README.md
Normal file
78
modules/20-services-apps/portainer/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Portainer Module
|
||||
|
||||
This module deploys [Portainer](https://www.portainer.io/), a lightweight management UI that allows you to easily manage your different Docker environments.
|
||||
|
||||
## Overview
|
||||
|
||||
The Portainer module:
|
||||
|
||||
- Deploys one Docker container: `portainer`.
|
||||
- Mounts the Docker socket to allow Portainer to manage the Docker environment.
|
||||
- Persists Portainer data to a volume on the host.
|
||||
- Provides a service definition for integration with networking modules.
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "portainer" {
|
||||
source = "./modules/20-services-apps/portainer"
|
||||
volume_path = "/path/to/volumes/portainer"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------- | ---------------------------------------------------------------- |
|
||||
| `image_tag` | Tag of the Portainer image to use |
|
||||
| `volume_path` | Host path for Portainer data volume |
|
||||
| `networks` | List of additional networks to which Portainer should be attached |
|
||||
|
||||
## 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 = "portainer"
|
||||
primary_port = 9000
|
||||
endpoint = "http://portainer:9000"
|
||||
subdomains = ["portainer"]
|
||||
publish_via = "reverse_proxy"
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Portainer stores its data in a single volume:
|
||||
|
||||
1. Portainer data: `/data` in the container, mapped to `${volume_path}/data` on the host.
|
||||
|
||||
It also mounts the Docker socket from `/var/run/docker.sock` on the host to `/var/run/docker.sock` in the container to manage Docker.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "portainer" {
|
||||
source = "./modules/20-services-apps/portainer"
|
||||
volume_path = "${module.system_globals.volume_host}/portainer"
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.portainer.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
67
modules/20-services-apps/portainer/main.tf
Normal file
67
modules/20-services-apps/portainer/main.tf
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
variable "image_tag" {
|
||||
description = "The tag for the portainer container image"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "portainer"
|
||||
image = "portainer/portainer-ce"
|
||||
tag = var.image_tag
|
||||
monitoring = true
|
||||
internal_port = 9000
|
||||
exposed_port = 9000
|
||||
|
||||
# Define volumes
|
||||
volumes = [
|
||||
{
|
||||
host_path = "/var/run/docker.sock"
|
||||
container_path = "/var/run/docker.sock"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = "${var.volume_path}/data"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Create the portainer container
|
||||
module "portainer" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [
|
||||
{
|
||||
internal = local.internal_port
|
||||
external = local.exposed_port
|
||||
protocol = "tcp"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
109
services/main.tf
Normal file → Executable file
109
services/main.tf
Normal file → Executable file
@@ -1,6 +1,8 @@
|
||||
locals {
|
||||
module_dir = "../modules"
|
||||
volume_host = module.system_globals.volume_host
|
||||
root_volume = module.system_globals.volume_host
|
||||
volume_host = "${module.system_globals.volume_host}/appdata"
|
||||
data_host = "${module.system_globals.volume_host}/data"
|
||||
}
|
||||
|
||||
module "system_globals" {
|
||||
@@ -18,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"
|
||||
@@ -30,15 +42,73 @@ 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"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "copyparty" {
|
||||
source = "${local.module_dir}/20-services-apps/copyparty"
|
||||
fileshare_path = local.root_volume
|
||||
config_path = "${local.volume_host}/copyparty"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "crawl4ai" {
|
||||
source = "${local.module_dir}/20-services-apps/crawl4ai"
|
||||
volume_path = "${local.volume_host}/crawl4ai"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "emulatorjs" {
|
||||
source = "${local.module_dir}/20-services-apps/emulatorjs"
|
||||
volume_path = "${local.volume_host}/emulatorjs"
|
||||
image_tag = "1.9.2"
|
||||
}
|
||||
|
||||
module "glance" {
|
||||
source = "${local.module_dir}/20-services-apps/glance"
|
||||
volume_path = "${local.volume_host}/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"
|
||||
library_path = "${local.data_host}/media/photos"
|
||||
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" {
|
||||
@@ -47,12 +117,30 @@ module "linkwarden" {
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "n8n" {
|
||||
source = "${local.module_dir}/20-services-apps/n8n"
|
||||
volume_path = "${local.volume_host}/n8n"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "nocodb" {
|
||||
source = "${local.module_dir}/20-services-apps/nocodb"
|
||||
volume_path = "${local.volume_host}/nocodb"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "ntfy" {
|
||||
source = "${local.module_dir}/20-services-apps/ntfy"
|
||||
volume_path = "${local.volume_host}/ntfy"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "portainer" {
|
||||
source = "${local.module_dir}/20-services-apps/portainer"
|
||||
volume_path = "${local.volume_host}/portainer"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "pterodactyl_panel" {
|
||||
source = "${local.module_dir}/20-services-apps/pterodactyl/panel"
|
||||
volume_path = "${local.volume_host}/pterodactyl/panel"
|
||||
@@ -65,10 +153,21 @@ module "pterodactyl_wings" {
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "n8n" {
|
||||
source = "${local.module_dir}/20-services-apps/n8n"
|
||||
volume_path = "${local.volume_host}/n8n"
|
||||
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" {
|
||||
|
||||
13
services/outputs.tf
Normal file → Executable file
13
services/outputs.tf
Normal file → Executable file
@@ -6,13 +6,24 @@ 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,
|
||||
module.nocodb.service_definition,
|
||||
module.ntfy.service_definition,
|
||||
module.portainer.service_definition,
|
||||
module.pterodactyl_wings.service_definition,
|
||||
module.pterodactyl_panel.service_definition,
|
||||
module.n8n.service_definition,
|
||||
module.qbittorrent.service_definition,
|
||||
module.sabnzbd.service_definition,
|
||||
module.searxng.service_definition
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user