Compare commits

1 Commits

Author SHA1 Message Date
Yuris Cakranegara
c07f59d021 feat: add media server 2025-06-27 21:34:55 +10:00
56 changed files with 1428 additions and 2812 deletions

View File

@@ -19,10 +19,6 @@ 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
@@ -44,7 +40,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 = local.volume_host
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
monitoring = true
}

View File

@@ -63,6 +63,10 @@ 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
@@ -111,12 +115,12 @@ module "caddy" {
ports = [
{
external = "80"
external = "9080"
internal = "80"
protocol = "tcp"
},
{
external = "443"
external = "9443"
internal = "443"
protocol = "tcp"
}

View File

@@ -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 :

View File

@@ -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,36 +139,15 @@ 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
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
}
}
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
# Set log options
log_driver = var.log_driver

View File

@@ -179,48 +179,12 @@ 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"
@@ -231,8 +195,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"
}
}

View File

@@ -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"
}
}

View File

@@ -193,6 +193,6 @@ output "service_definition" {
endpoint = "http://${local.container_name}:${local.affine_internal_port}"
subdomains = ["notes"]
publish_via = "reverse_proxy"
proxied = true
proxied = false
}
}

View File

@@ -1,34 +0,0 @@
# 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=

View File

@@ -1,140 +0,0 @@
# *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`.

View File

@@ -1,280 +0,0 @@
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
}
}

View File

@@ -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 = true
proxied = false
}
}

View File

@@ -1,85 +0,0 @@
# 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
]
}
```

View File

@@ -1,89 +0,0 @@
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"
}
}

View File

@@ -1,18 +0,0 @@
# 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=

View File

@@ -1,91 +0,0 @@
# 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
]
}
```

View File

@@ -1,99 +0,0 @@
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}"
}
}

View File

@@ -1,77 +0,0 @@
# 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
]
}
```

View File

@@ -1,52 +0,0 @@
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"
}
}

View File

@@ -1,33 +0,0 @@
# 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

View File

@@ -1,65 +0,0 @@
# 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).

View File

@@ -1,88 +0,0 @@
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
}

View File

@@ -1,14 +0,0 @@
# 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

View File

@@ -1,110 +0,0 @@
# 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
]
}

View File

@@ -1,197 +0,0 @@
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
}
}

View File

@@ -1,4 +0,0 @@
# Optional values for Jellyfin module
# Only needed if you enable JELLYFIN_PublishedServerUrl in main.tf
HOSTNAME=example.com

View File

@@ -1,87 +0,0 @@
# 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`.

View File

@@ -1,96 +0,0 @@
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
}
}

View File

@@ -0,0 +1,180 @@
# Media Server Module
This module deploys a complete media server stack using Docker containers in the homelab environment. It includes content management services (Sonarr, Radarr, Readarr), content discovery (Prowlarr, Jellyseerr), download clients (Qbittorrent, Sabnzbd), and media playback (Jellyfin).
## Overview
The Media Server module:
- Deploys multiple Docker containers:
- `sonarr`: Series/TV management system
- `radarr`: Movie management system
- `readarr`: Book management system
- `jellyseerr`: Content request and discovery system
- `prowlarr`: Indexer management system
- `qbittorrent`: Torrent download client with VueTorrent UI
- `unpackerr`: Automatic extraction utility
- `jellyfin`: Media server for streaming content
- `sabnzbd`: Usenet download client
- `flaresolverr`: Proxy service to bypass Cloudflare and other protection
- `autoheal`: Service for automatic container health restarts
- Creates a dedicated Docker network (`media-server`) for container communication
- Persists data to volumes on the host
- Provides service definitions for integration with networking modules
- Exposes Jellyfin, Jellyseerr, and Sabnzbd via reverse proxy
## Usage
```hcl
module "media_server" {
source = "./modules/20-services-apps/media-server"
volume_path = "/path/to/app/data"
data_root = "/path/to/media/data"
download_root = "/path/to/download/data"
user_id = "1000"
group_id = "1000"
timezone = "UTC"
hostname = "example.com"
sonarr_api_key = "your-sonarr-api-key"
radarr_api_key = "your-radarr-api-key"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ---------------- | ------------------------------------------------- | -------------- | -------- |
| `volume_path` | Base directory for application data | `string` | - |
| `data_root` | Root directory for media data | `string` | - |
| `download_root` | Directory for downloads | `string` | - |
| `user_id` | User ID for container permissions | `string` | `"1000"` |
| `group_id` | Group ID for container permissions | `string` | `"1000"` |
| `timezone` | Timezone for the containers | `string` | `"UTC"` |
| `hostname` | Hostname for the Jellyfin PublishedServerUrl | `string` | - |
| `sonarr_api_key` | API key for Sonarr | `string` | - |
| `radarr_api_key` | API key for Radarr | `string` | - |
| `networks` | List of networks to which containers are attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| --------------------- | ------------------------------------------------------------------ |
| `service_definitions` | Service definitions for Jellyfin, Jellyseerr, and Sabnzbd services |
| `network_name` | Name of the media server network |
## Services
### Sonarr
TV series management tool that integrates with various download clients and indexers to automate obtaining TV episodes.
- Port: 8989
- Volumes:
- Config: `/config``${volume_path}/sonarr`
- Data: `/data``${data_root}`
### Radarr
Movie management tool that integrates with various download clients and indexers to automate obtaining movies.
- Port: 7878
- Volumes:
- Config: `/config``${volume_path}/radarr`
- Data: `/data``${data_root}`
### Readarr
Book management tool that integrates with various download clients and indexers to automate obtaining books.
- Port: 8787
- Volumes:
- Config: `/config``${volume_path}/readarr`
- Books: `/books``${data_root}`
### Jellyseerr
Media request system that integrates with Jellyfin to allow users to request new content.
- Port: 5055
- Volumes:
- Config: `/app/config``${volume_path}/jellyseerr`
### Prowlarr
Indexer manager/proxy that integrates with various PVR apps and download clients.
- Port: 9696
- Volumes:
- Config: `/config``${volume_path}/prowlarr`
### FlareSolverr
Proxy server to bypass Cloudflare and other protection methods.
- Port: 8191
### QBittorrent
Torrent client with VueTorrent web interface.
- Port: 8080
- Volumes:
- Config: `/config``${volume_path}/qbittorrent`
- Downloads: `/data/torrents``${download_root}`
### Unpackerr
Extracts completed downloads automatically for Sonarr, Radarr, and others.
- Volumes:
- Downloads: `/data/torrents``${download_root}`
### Jellyfin
Media server for streaming content to various devices.
- Ports:
- 8096 (HTTP)
- 7359 (UDP)
- 1900 (UDP)
- Volumes:
- Config: `/config``${volume_path}/jellyfin`
- Data: `/data``${data_root}`
- Devices:
- `/dev/dri/` for hardware acceleration
### Sabnzbd
Usenet download client.
- Port: 6789 (external) → 8080 (internal)
- Volumes:
- Config: `/config``${volume_path}/sabnzbd/config`
- Downloads: `/downloads``${data_root}/usenet/downloads`
### Autoheal
Service that automatically checks for container health and restarts unhealthy containers.
- Requires access to Docker socket
## Networking
The module creates a dedicated Docker network named `media-server` for communication between the components. Each service container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## Reverse Proxy Integration
Three services are configured to be exposed through a reverse proxy:
- **Jellyfin**: Exposed at subdomain `jellyfin`
- **Jellyseerr**: Exposed at subdomain `requests`
- **Sabnzbd**: Exposed at subdomain `sabnzbd`
These services have their `publish_via` property set to `"reverse_proxy"` in their service definitions.
## Example Integration in Main Configuration
```hcl
module "media_server" {
source = "./modules/20-services-apps/media-server"
volume_path = module.system_globals.volume_host
data_root = "${module.system_globals.data_root}/media"
download_root = "${module.system_globals.data_root}/downloads"
user_id = module.system_globals.user_id
group_id = module.system_globals.group_id
timezone = module.system_globals.timezone
hostname = "example.com"
sonarr_api_key = var.sonarr_api_key
radarr_api_key = var.radarr_api_key
networks = [module.services.homelab_docker_network_name]
}
# The service definitions are automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = concat(
module.media_server.service_definitions,
# Other service definitions
)
}
```

View File

@@ -0,0 +1,148 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
locals {
monitoring = true
# Define common healthcheck settings
healthcheck_interval = "30s"
healthcheck_retries = 10
}
# Create dedicated network for media server components
module "media_server_network" {
source = "../../01-networking/docker-network"
name = "media-server"
subnet = "11.102.0.0/16"
driver = "bridge"
}
# Import service modules
module "sonarr" {
source = "./services/sonarr"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
data_root = var.data_root
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "radarr" {
source = "./services/radarr"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
data_root = var.data_root
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "readarr" {
source = "./services/readarr"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
data_root = var.data_root
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "jellyseerr" {
source = "./services/jellyseerr"
timezone = var.timezone
volume_path = var.volume_path
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "prowlarr" {
source = "./services/prowlarr"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "qbittorrent" {
source = "./services/qbittorrent"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
download_root = var.download_root
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "unpackerr" {
source = "./services/unpackerr"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
download_root = var.download_root
sonarr_api_key = var.sonarr_api_key
radarr_api_key = var.radarr_api_key
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "jellyfin" {
source = "./services/jellyfin"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
data_root = var.data_root
hostname = var.hostname
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "sabnzbd" {
source = "./services/sabnzbd"
user_id = var.user_id
group_id = var.group_id
timezone = var.timezone
volume_path = var.volume_path
data_root = var.data_root
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "flaresolverr" {
source = "./services/flaresolverr"
timezone = var.timezone
log_level = "info"
log_html = "false"
captcha_solver = "none"
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}
module "autoheal" {
source = "./services/autoheal"
networks = concat([module.media_server_network.name], var.networks)
monitoring = local.monitoring
}

View File

@@ -0,0 +1,13 @@
output "service_definitions" {
description = "Service definitions for integration with networking modules"
value = [
module.jellyfin.service_definition,
module.jellyseerr.service_definition,
module.sabnzbd.service_definition
]
}
output "network_name" {
description = "Name of the media server network"
value = module.media_server_network.name
}

View File

@@ -0,0 +1,48 @@
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "autoheal"
image = "willfarrell/autoheal"
tag = "latest"
autoheal_env_vars = {
AUTOHEAL_CONTAINER_LABEL = "all"
}
autoheal_volumes = [
{
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
read_only = false
}
]
}
module "autoheal" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.autoheal_volumes
env_vars = local.autoheal_env_vars
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for autoheal"
value = {
name = local.container_name
endpoint = "http://${local.container_name}"
}
}

View File

@@ -0,0 +1,71 @@
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
variable "log_level" {
description = "Log level for flaresolverr"
type = string
default = "info"
}
variable "log_html" {
description = "Whether to log HTML"
type = string
default = "false"
}
variable "captcha_solver" {
description = "Type of CAPTCHA solver to use"
type = string
default = "none"
}
locals {
container_name = "flaresolverr"
image = "ghcr.io/flaresolverr/flaresolverr"
tag = "latest"
flaresolverr_env_vars = {
LOG_LEVEL = var.log_level
LOG_HTML = var.log_html
CAPTCHA_SOLVER = var.captcha_solver
TZ = var.timezone
}
}
module "flaresolverr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
env_vars = local.flaresolverr_env_vars
ports = [{
internal = 8191
external = 8191
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for flaresolverr"
value = {
name = local.container_name
primary_port = 8191
endpoint = "http://${local.container_name}:8191"
}
}

View File

@@ -0,0 +1,114 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "data_root" {
description = "Root directory for media data"
type = string
}
variable "hostname" {
description = "Hostname for the Jellyfin PublishedServerUrl"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "jellyfin"
image = "jellyfin/jellyfin"
tag = "latest"
internal_ports = [
{
internal = 8096
external = 8096
protocol = "tcp"
},
{
internal = 7359
external = 7359
protocol = "udp"
},
{
internal = 1900
external = 1900
protocol = "udp"
}
]
jellyfin_volumes = [
{
host_path = "${var.volume_path}/jellyfin"
container_path = "/config"
read_only = false
},
{
host_path = "${var.data_root}"
container_path = "/data"
read_only = false
}
]
jellyfin_devices = [
"/dev/dri/:/dev/dri/"
]
jellyfin_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
JELLYFIN_PublishedServerUrl = "${var.hostname}/jellyfin"
}
}
module "jellyfin" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.jellyfin_volumes
env_vars = local.jellyfin_env_vars
ports = local.internal_ports
devices = local.jellyfin_devices
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for integration with networking modules"
value = {
name = local.container_name
primary_port = 8096
endpoint = "http://${local.container_name}:8096"
subdomains = ["jellyfin"]
publish_via = "reverse_proxy"
proxied = false
}
}

View File

@@ -0,0 +1,77 @@
variable "timezone" {
description = "Timezone for the container"
type = string
}
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)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "jellyseerr"
image = "fallenbagel/jellyseerr"
tag = "latest"
jellyseerr_volumes = [
{
host_path = "${var.volume_path}/jellyseerr"
container_path = "/app/config"
read_only = false
}
]
jellyseerr_env_vars = {
LOG_LEVEL = "debug"
TZ = var.timezone
}
jellyseerr_healthcheck = {
test = ["CMD", "wget", "http://127.0.0.1:5055/api/v1/status", "-qO", "/dev/null"]
interval = "30s"
timeout = "5s"
retries = 10
start_period = "5s"
}
}
module "jellyseerr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.jellyseerr_volumes
env_vars = local.jellyseerr_env_vars
healthcheck = local.jellyseerr_healthcheck
ports = [{
internal = 5055
external = 5055
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for integration with networking modules"
value = {
name = local.container_name
primary_port = 5055
endpoint = "http://${local.container_name}:5055"
subdomains = ["requests"]
publish_via = "reverse_proxy"
proxied = false
}
}

View File

@@ -0,0 +1,85 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
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)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "prowlarr"
image = "lscr.io/linuxserver/prowlarr"
tag = "latest"
prowlarr_volumes = [
{
host_path = "${var.volume_path}/prowlarr"
container_path = "/config"
read_only = false
}
]
prowlarr_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
}
prowlarr_healthcheck = {
test = ["CMD", "curl", "--fail", "http://127.0.0.1:9696/prowlarr/ping"]
interval = "30s"
timeout = "5s"
retries = 10
start_period = "5s"
}
}
module "prowlarr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.prowlarr_volumes
env_vars = local.prowlarr_env_vars
healthcheck = local.prowlarr_healthcheck
ports = [{
internal = 9696
external = 9696
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for prowlarr"
value = {
name = local.container_name
primary_port = 9696
endpoint = "http://${local.container_name}:9696"
}
}

View File

@@ -0,0 +1,97 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "download_root" {
description = "Directory for downloads"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "qbittorrent"
image = "lscr.io/linuxserver/qbittorrent"
tag = "libtorrentv1"
qbittorrent_volumes = [
{
host_path = "${var.volume_path}/qbittorrent"
container_path = "/config"
read_only = false
},
{
host_path = var.download_root
container_path = "/data/torrents"
read_only = false
}
]
qbittorrent_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
WEBUI_PORT = "8080"
DOCKER_MODS = "ghcr.io/gabe565/linuxserver-mod-vuetorrent"
}
qbittorrent_healthcheck = {
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
interval = "30s"
timeout = "5s"
retries = 10
start_period = "5s"
}
}
module "qbittorrent" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.qbittorrent_volumes
env_vars = local.qbittorrent_env_vars
healthcheck = local.qbittorrent_healthcheck
ports = [{
internal = 8080
external = 8080
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for qbittorrent"
value = {
name = local.container_name
primary_port = 8080
endpoint = "http://${local.container_name}:8080"
}
}

View File

@@ -0,0 +1,95 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "data_root" {
description = "Root directory for media data"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "radarr"
image = "lscr.io/linuxserver/radarr"
tag = "latest"
radarr_volumes = [
{
host_path = "${var.volume_path}/radarr"
container_path = "/config"
read_only = false
},
{
host_path = var.data_root
container_path = "/data"
read_only = false
}
]
radarr_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
}
radarr_healthcheck = {
test = ["CMD", "curl", "--fail", "http://127.0.0.1:7878/radarr/ping"]
interval = "30s"
timeout = "5s"
retries = 10
start_period = "5s"
}
}
module "radarr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.radarr_volumes
env_vars = local.radarr_env_vars
healthcheck = local.radarr_healthcheck
ports = [{
internal = 7878
external = 7878
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for radarr"
value = {
name = local.container_name
primary_port = 7878
endpoint = "http://${local.container_name}:7878"
}
}

View File

@@ -0,0 +1,86 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "data_root" {
description = "Root directory for media data"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "readarr"
image = "lscr.io/linuxserver/readarr"
tag = "develop"
readarr_volumes = [
{
host_path = "${var.volume_path}/readarr"
container_path = "/config"
read_only = false
},
{
host_path = var.data_root
container_path = "/books"
read_only = false
}
]
readarr_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
}
}
module "readarr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.readarr_volumes
env_vars = local.readarr_env_vars
ports = [{
internal = 8787
external = 8787
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for readarr"
value = {
name = local.container_name
primary_port = 8787
endpoint = "http://${local.container_name}:8787"
}
}

View File

@@ -0,0 +1,89 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "data_root" {
description = "Root directory for media data"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "sabnzbd"
image = "lscr.io/linuxserver/sabnzbd"
tag = "latest"
sabnzbd_volumes = [
{
host_path = "${var.volume_path}/sabnzbd/config"
container_path = "/config"
read_only = false
},
{
host_path = "${var.data_root}/usenet/downloads"
container_path = "/downloads"
read_only = false
}
]
sabnzbd_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
}
}
module "sabnzbd" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.sabnzbd_volumes
env_vars = local.sabnzbd_env_vars
ports = [{
internal = 8080
external = 6789
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "unless-stopped"
}
output "service_definition" {
description = "Service definition for integration with networking modules"
value = {
name = local.container_name
primary_port = 8080
endpoint = "http://${local.container_name}:8080"
subdomains = ["sabnzbd"]
publish_via = "reverse_proxy"
proxied = false
}
}

View File

@@ -0,0 +1,95 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "data_root" {
description = "Root directory for media data"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "sonarr"
image = "lscr.io/linuxserver/sonarr"
tag = "latest"
sonarr_volumes = [
{
host_path = "${var.volume_path}/sonarr"
container_path = "/config"
read_only = false
},
{
host_path = var.data_root
container_path = "/data"
read_only = false
}
]
sonarr_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
}
sonarr_healthcheck = {
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8989/sonarr/ping"]
interval = "30s"
timeout = "5s"
retries = 10
start_period = "5s"
}
}
module "sonarr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.sonarr_volumes
env_vars = local.sonarr_env_vars
healthcheck = local.sonarr_healthcheck
ports = [{
internal = 8989
external = 8989
protocol = "tcp"
}]
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "Service definition for sonarr"
value = {
name = local.container_name
primary_port = 8989
endpoint = "http://${local.container_name}:8989"
}
}

View File

@@ -0,0 +1,90 @@
variable "user_id" {
description = "User ID for container permissions"
type = string
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
}
variable "timezone" {
description = "Timezone for the container"
type = string
}
variable "download_root" {
description = "Directory for downloads"
type = string
}
variable "sonarr_api_key" {
description = "API key for Sonarr"
type = string
sensitive = true
}
variable "radarr_api_key" {
description = "API key for Radarr"
type = string
sensitive = true
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
variable "monitoring" {
description = "Enable container monitoring"
type = bool
default = true
}
locals {
container_name = "unpackerr"
image = "golift/unpackerr"
tag = "latest"
unpackerr_volumes = [
{
host_path = var.download_root
container_path = "/data/torrents"
read_only = false
}
]
unpackerr_env_vars = {
TZ = var.timezone
UN_SONARR_0_URL = "http://sonarr:8989/sonarr"
UN_SONARR_0_API_KEY = var.sonarr_api_key
UN_RADARR_0_URL = "http://radarr:7878/radarr"
UN_RADARR_0_API_KEY = var.radarr_api_key
}
unpackerr_security_opts = [
"no-new-privileges:true"
]
}
module "unpackerr" {
source = "../../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.unpackerr_volumes
env_vars = local.unpackerr_env_vars
security_opts = local.unpackerr_security_opts
networks = var.networks
monitoring = var.monitoring
restart_policy = "always"
user = "${var.user_id}:${var.group_id}"
}
output "service_definition" {
description = "Service definition for unpackerr"
value = {
name = local.container_name
endpoint = "http://${local.container_name}"
}
}

View File

@@ -0,0 +1,55 @@
variable "volume_path" {
description = "Base directory for volumes (APP_DATA)"
type = string
}
variable "data_root" {
description = "Root directory for media data (DATA_ROOT)"
type = string
}
variable "download_root" {
description = "Directory for downloads (DOWNLOAD_ROOT)"
type = string
}
variable "user_id" {
description = "User ID for container permissions"
type = string
default = "1000"
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
default = "1000"
}
variable "timezone" {
description = "Timezone for the containers"
type = string
default = "UTC"
}
variable "hostname" {
description = "Hostname for the Jellyfin PublishedServerUrl"
type = string
}
variable "sonarr_api_key" {
description = "API key for Sonarr"
type = string
sensitive = true
}
variable "radarr_api_key" {
description = "API key for Radarr"
type = string
sensitive = true
}
variable "networks" {
description = "List of additional networks to which containers should be attached"
type = list(string)
default = []
}

View File

@@ -8,7 +8,3 @@ N8N_PORT=5678
N8N_PROTOCOL=http
WEBHOOK_URL=https://n8n.yourdomain.com/
NODE_FUNCTION_ALLOW_EXTERNAL=*
# MCP
N8N_MCP_AUTH_TOKEN=
N8N_API_KEY=

View File

@@ -1,19 +1,17 @@
# n8n Module
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.
This module deploys [n8n](https://n8n.io/), a workflow automation tool for technical people, as Docker containers in the homelab environment.
## Overview
The n8n module:
- Deploys four Docker containers:
- Deploys two 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 definitions for integration with networking modules
- Provides service definition for integration with networking modules
## Usage
@@ -30,21 +28,18 @@ module "n8n" {
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the n8n image to use | `string` | `"latest"` |
| `volume_path` | Host path for n8n, Postgres, Redis, and n8n-mcp data volumes | `string` | - |
| `volume_path` | Host path for n8n and Postgres data volumes | `string` | - |
| `networks` | List of additional networks to which n8n should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| ---------------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for the n8n container |
| `n8n_mcp_service_definition` | Service definition for the n8n-mcp container |
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definitions
## Service Definition
This module outputs two service definitions that are used by the networking modules to expose the services.
### n8n
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
@@ -52,63 +47,45 @@ This module outputs two service definitions that are used by the networking modu
primary_port = 5678
endpoint = "http://n8n:5678"
subdomains = ["n8n"]
publish_via = "tunnel"
}
```
### n8n-mcp
```hcl
{
name = "n8n-mcp"
primary_port = 3000
endpoint = "http://n8n-mcp:3000"
subdomains = ["n8n-mcp"]
publish_via = "tunnel"
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Environment Variables
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:
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:
- **Database configuration (`n8n-postgres`)**:
- Database configuration:
- `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`)**:
- n8n configuration:
- `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
The services store data in several volumes:
n8n stores its data in two main 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
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
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
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 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.
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.
## Integration with Networking Modules
The services are configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
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
@@ -119,41 +96,13 @@ module "n8n" {
networks = [module.services.homelab_docker_network_name]
}
# The service definitions are automatically included in the services output
# The service definition is 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": "..."
}
}
}
}
```

View File

@@ -23,24 +23,17 @@ variable "networks" {
default = []
}
module "system_globals" {
source = "../../00-globals/system"
}
locals {
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"
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"
n8n_internal_port = 5678
# Define volumes
n8n_volumes = [
{
@@ -49,7 +42,7 @@ locals {
read_only = false
}
]
database_volumes = [
{
host_path = "${var.volume_path}/db_storage/_data"
@@ -62,34 +55,29 @@ 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)
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"
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)
}
# Healthcheck configuration for the database
@@ -100,66 +88,12 @@ 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
@@ -169,31 +103,12 @@ 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"
@@ -201,26 +116,10 @@ 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, 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]
depends_on = [module.postgres]
}
output "service_definition" {
@@ -233,14 +132,3 @@ 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"
}
}

View File

@@ -1,4 +0,0 @@
# Database Configuration
DB_USERNAME=postgres
DB_PASSWORD=change_this_password
DB_DATABASE=root_db

View File

@@ -1,106 +0,0 @@
# 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
]
}
```

View File

@@ -1,129 +0,0 @@
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"
}
}

View File

@@ -1,78 +0,0 @@
# 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
]
}
```

View File

@@ -1,67 +0,0 @@
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}"
}
}

View File

@@ -1,117 +0,0 @@
# 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`.

View File

@@ -1,86 +0,0 @@
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}"
}
}

View File

@@ -1,78 +0,0 @@
# 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`.

View File

@@ -1,69 +0,0 @@
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 Executable file → Normal file
View File

@@ -1,8 +1,6 @@
locals {
module_dir = "../modules"
root_volume = module.system_globals.volume_host
volume_host = "${module.system_globals.volume_host}/appdata"
data_host = "${module.system_globals.volume_host}/data"
volume_host = module.system_globals.volume_host
}
module "system_globals" {
@@ -20,16 +18,6 @@ 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"
@@ -42,73 +30,15 @@ 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" {
@@ -117,30 +47,12 @@ 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"
@@ -153,21 +65,10 @@ module "pterodactyl_wings" {
networks = [module.homelab_docker_network.name]
}
module "qbittorrent" {
source = "${local.module_dir}/20-services-apps/qbittorrent"
volume_path = "${local.volume_host}/qbittorrent"
downloads_path = "${local.data_host}/torrents"
networks = [module.media_docker_network.name]
connect_via_gluetun = true
gluetun_container_name = "gluetun"
depends_on = [module.gluetun]
}
module "sabnzbd" {
source = "${local.module_dir}/20-services-apps/sabnzbd"
volume_path = "${local.volume_host}/sabnzbd"
downloads_path = "${local.data_host}/usenet/downloads"
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
module "n8n" {
source = "${local.module_dir}/20-services-apps/n8n"
volume_path = "${local.volume_host}/n8n"
networks = [module.homelab_docker_network.name]
}
module "searxng" {

13
services/outputs.tf Executable file → Normal file
View File

@@ -6,24 +6,13 @@ 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.qbittorrent.service_definition,
module.sabnzbd.service_definition,
module.n8n.service_definition,
module.searxng.service_definition
]
}