This commit is contained in:
2025-10-03 15:49:36 +01:00
parent bce43c4a71
commit 0fe34fb0e4
45 changed files with 10 additions and 3706 deletions

View File

@@ -60,7 +60,7 @@ homelab/
│ └── docker-service/ # Generic module for deploying Docker containers │ └── docker-service/ # Generic module for deploying Docker containers
└── 20-services-apps/ # Application-specific wrapper modules └── 20-services-apps/ # Application-specific wrapper modules
├── jellyfin/ ├── jellyfin/
├── affine/ ├── calibre/
└── ... # Other application modules └── ... # Other application modules
└── services/ # Application services (Docker containers) └── services/ # Application services (Docker containers)

View File

@@ -101,14 +101,14 @@ The `publish_via` field controls which networking module(s) will expose the serv
### Basic Service with Default Settings ### Basic Service with Default Settings
```hcl ```hcl
# Example based on ntfy (reverse-proxy only with direct IP exposure) # Example based on jellyfin (reverse-proxy only with direct IP exposure)
output "service_definition" { output "service_definition" {
description = "Service definition for a notification service" description = "Service definition for a media server"
value = { value = {
name = "ntfy" name = "jellyfin"
primary_port = 80 primary_port = 8096
endpoint = "http://ntfy:80" endpoint = "http://jellyfin:8096"
subdomains = ["ntfy"] subdomains = ["media"]
publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy
proxied = false # Don't proxy through Cloudflare (expose direct IP) proxied = false # Don't proxy through Cloudflare (expose direct IP)
} }

View File

@@ -72,8 +72,8 @@ module "homelab_tunnel" {
tunnel_name = "homelab-tunnel" tunnel_name = "homelab-tunnel"
ingress_rules = [ ingress_rules = [
{ {
hostname = "budget.${module.cloudflare_globals.domain}" hostname = "media.${module.cloudflare_globals.domain}"
service = "http://actualbudget:5006" service = "http://jellyfin:8096"
} }
] ]
} }

View File

@@ -1,77 +0,0 @@
# ActualBudget Module
This module deploys [ActualBudget](https://actualbudget.com/), a personal finance and budgeting application, as a Docker container in the homelab environment.
## Overview
The ActualBudget module:
- Deploys the `actualbudget/actual-server` Docker container
- Persists data to a volume on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "actualbudget" {
source = "./modules/20-services-apps/actualbudget"
volume_path = "/path/to/volumes/actualbudget"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the ActualBudget image to use | `string` | `"latest"` |
| `volume_path` | Host path for ActualBudget 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 = "actualbudget"
primary_port = 5006
endpoint = "http://actualbudget:5006"
subdomains = ["budget"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Data Persistence
ActualBudget stores its data in the `/data` directory inside the container. This is mapped to a volume on the host at `${volume_path}/data`.
## 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 "actualbudget" {
source = "./modules/20-services-apps/actualbudget"
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.actualbudget.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,52 +0,0 @@
variable "image_tag" {
description = "Tag of the ActualBudget image to use"
type = string
default = "latest"
}
variable "volume_path" {
description = "Host path for ActualBudget data volume"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
locals {
container_name = "actualbudget"
image = "actualbudget/actual-server"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
exposed_port = 5006
subdomains = ["budget"]
default_volumes = [
{
container_path = "/data"
host_path = "${var.volume_path}/data"
read_only = false
}
]
}
module "actualbudget" {
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.exposed_port
endpoint = "http://${local.container_name}:${local.exposed_port}"
subdomains = local.subdomains
publish_via = "tunnel"
}
}

View File

@@ -1,17 +0,0 @@
# Affine Configuration
AFFINE_REVISION=canary
PORT=3010
AFFINE_SERVER_HTTPS=true
AFFINE_SERVER_HOST=affine.yourdomain.com
AFFINE_SERVER_NAME='AFFiNE Selfhosted'
# Database Configuration
DB_USERNAME=affine
DB_PASSWORD=change_this_password
DB_DATABASE=affine
# R2 Configuration
R2_OBJECT_STORAGE_ACCOUNT_ID=
R2_OBJECT_STORAGE_ACCESS_KEY_ID=
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY=

View File

@@ -1,121 +0,0 @@
# AFFiNE Module
This module deploys [AFFiNE](https://affine.pro/), a privacy-first, local-first, note-taking and knowledge base application, as Docker containers in the homelab environment.
## Overview
The AFFiNE module:
- Deploys four Docker containers:
- `affine_server`: The main AFFiNE application server
- `affine_migration_job`: A container that runs pre-deployment migrations
- `affine_postgres`: A PostgreSQL (pgvector) database backend
- `affine_redis`: A Redis instance for caching and temporary data
- Creates a dedicated Docker network (`affine-network`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "affine" {
source = "./modules/20-services-apps/affine"
volume_path = "/path/to/volumes/affine"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | -------------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the AFFiNE image to use | `string` | `"stable"` |
| `volume_path` | Host path for AFFiNE and database data volumes | `string` | - |
| `networks` | List of additional networks to which AFFiNE 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 = "affine_server"
primary_port = 3010
endpoint = "http://affine_server:3010"
subdomains = ["affine"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Environment Variables
AFFiNE 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 "affine")
- AFFiNE configuration:
- `AFFINE_REVISION`: Version of AFFiNE to use ("stable" or "canary") (defaults to "canary")
- `PORT`: External port for the AFFiNE server (defaults to 3010)
- `AFFINE_SERVER_HTTPS`: Whether to use HTTPS (defaults to "true")
- `AFFINE_SERVER_HOST`: Hostname for the AFFiNE server
- `AFFINE_SERVER_NAME`: Name for the AFFiNE server (defaults to "AFFiNE Selfhosted")
- Cloudflare R2 configuration:
- `R2_OBJECT_STORAGE_ACCOUNT_ID`: Cloudflare R2 account ID
- `R2_OBJECT_STORAGE_ACCESS_KEY_ID`: Cloudflare R2 access key ID
- `R2_OBJECT_STORAGE_SECRET_ACCESS_KEY`: Cloudflare R2 secret access key
## Data Persistence
AFFiNE stores its data in three main volumes:
1. AFFiNE application data: `/root/.affine/storage` in the container, mapped to `${volume_path}/self-host/storage` on the host
2. AFFiNE configuration: `/root/.affine/config` in the container, mapped to `${volume_path}/self-host/config` on the host
3. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/self-host/postgres/pgdata` on the host
## Networking
The module creates a dedicated Docker network named `affine-network` for communication between the AFFiNE components. The AFFiNE 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 AFFiNE containers have the following dependencies:
- The main `affine_server` depends on PostgreSQL, Redis, and the migration job
- The migration job depends on PostgreSQL and Redis
- Both PostgreSQL and Redis use healthchecks to ensure they're ready before dependent services start
## 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 "affine" {
source = "./modules/20-services-apps/affine"
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.affine.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,198 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the affine container image"
type = string
default = "stable"
}
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 = []
}
module "smtp" {
source = "../../00-globals/smtp"
}
locals {
container_name = "affine-server"
migration_name = "affine-migration-job"
redis_name = "affine-redis"
postgres_name = "affine-postgres"
affine_image = "ghcr.io/yurisasc/affine-graphql"
postgres_image = "pgvector/pgvector"
redis_image = "redis"
affine_tag = provider::dotenv::get_by_key("AFFINE_REVISION", local.env_file)
postgres_tag = "pg16"
redis_tag = "latest"
monitoring = true
env_file = "${path.module}/.env"
affine_internal_port = 3010
# Define volumes
affine_volumes = [
{
host_path = "${var.volume_path}/self-host/storage"
container_path = "/root/.affine/storage"
read_only = false
},
{
host_path = "${var.volume_path}/self-host/config"
container_path = "/root/.affine/config"
read_only = false
}
]
migration_volumes = [
{
host_path = "${var.volume_path}/self-host/storage"
container_path = "/root/.affine/storage"
read_only = false
},
{
host_path = "${var.volume_path}/self-host/config"
container_path = "/root/.affine/config"
read_only = false
}
]
postgres_volumes = [
{
host_path = "${var.volume_path}/self-host/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", local.env_file)
POSTGRES_INITDB_ARGS = "--data-checksums"
POSTGRES_HOST_AUTH_METHOD = "trust"
}
# Environment variables for AFFiNE
affine_env_vars = {
REDIS_SERVER_HOST = local.redis_name
DATABASE_URL = "postgresql://${provider::dotenv::get_by_key("DB_USERNAME", local.env_file)}:${provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)}@${local.postgres_name}:5432/${provider::dotenv::get_by_key("DB_DATABASE", local.env_file)}"
AFFINE_INDEXER_ENABLED = "false"
AFFINE_SERVER_HTTPS = provider::dotenv::get_by_key("AFFINE_SERVER_HTTPS", local.env_file)
AFFINE_SERVER_HOST = provider::dotenv::get_by_key("AFFINE_SERVER_HOST", local.env_file)
AFFINE_SERVER_NAME = provider::dotenv::get_by_key("AFFINE_SERVER_NAME", local.env_file)
PORT = provider::dotenv::get_by_key("PORT", local.env_file)
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 = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
MAILER_HOST = module.smtp.mail_host
MAILER_PORT = module.smtp.mail_port
MAILER_USER = module.smtp.mail_username
MAILER_PASSWORD = module.smtp.mail_password
R2_OBJECT_STORAGE_ACCOUNT_ID = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_ACCOUNT_ID", local.env_file)
R2_OBJECT_STORAGE_ACCESS_KEY_ID = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_ACCESS_KEY_ID", local.env_file)
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_SECRET_ACCESS_KEY", local.env_file)
}
# Healthcheck configuration for Redis
redis_healthcheck = {
test = ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval = "10s"
timeout = "5s"
retries = 5
start_period = "5s"
}
# Healthcheck configuration for Postgres
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", local.env_file)]
interval = "10s"
timeout = "5s"
retries = 5
start_period = "5s"
}
}
module "affine_network" {
source = "../../01-networking/docker-network"
name = "affine-network"
subnet = "11.100.0.0/16"
driver = "bridge"
}
# 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
networks = [module.affine_network.name]
monitoring = local.monitoring
healthcheck = local.redis_healthcheck
}
# 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.affine_network.name]
monitoring = local.monitoring
healthcheck = local.postgres_healthcheck
}
# Create the migration job container
module "migration" {
source = "../../10-services-generic/docker-service"
container_name = local.migration_name
image = local.affine_image
tag = local.affine_tag
volumes = local.migration_volumes
env_vars = local.affine_env_vars
command = ["sh", "-c", "node ./scripts/self-host-predeploy.js"]
networks = [module.affine_network.name]
monitoring = local.monitoring
depends_on = [module.postgres, module.redis]
restart_policy = "no"
}
# Create the affine container
module "affine" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.affine_image
tag = local.affine_tag
volumes = local.affine_volumes
env_vars = local.affine_env_vars
networks = concat([module.affine_network.name], var.networks)
monitoring = local.monitoring
depends_on = [module.postgres, module.redis, module.migration]
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = local.affine_internal_port
endpoint = "http://${local.container_name}:${local.affine_internal_port}"
subdomains = ["notes"]
publish_via = "reverse_proxy"
proxied = true
}
}

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

@@ -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,3 +0,0 @@
EMULATORJS_FRONTEND_PORT=5823
EMULATORJS_CONFIG_PORT=5824
EMULATORJS_BACKEND_PORT=5825

View File

@@ -1,99 +0,0 @@
# EmulatorJS Module
This module deploys [EmulatorJS](https://github.com/linuxserver/docker-emulatorjs), a self-hosted retro gaming emulation platform, as a Docker container in the homelab environment.
## Overview
The EmulatorJS module:
- Deploys the `linuxserver/emulatorjs` Docker container
- Persists configuration and game data to volumes on the host
- Exposes multiple ports for frontend, configuration, and backend services
- Provides service definition for integration with networking modules
## Usage
```hcl
module "emulatorjs" {
source = "./modules/20-services-apps/emulatorjs"
volume_path = "/path/to/volumes/emulatorjs"
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | -------------------------------------------- | -------- | ---------- |
| `image_tag` | Tag of the EmulatorJS image to use | `string` | `"latest"` |
| `volume_path` | Host path for EmulatorJS data volumes | `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 = "emulatorjs"
primary_port = <frontend_port>
endpoint = "http://emulatorjs:<frontend_port>"
}
```
Note that unlike other services, EmulatorJS doesn't specify subdomains or a publish method in its service definition. This may require manual configuration in your networking setup.
## Ports
EmulatorJS exposes three ports, which are mapped to host ports defined in the `.env` file:
1. Frontend (port 80) - The main web interface for accessing games
2. Config (port 3000) - The configuration interface
3. Backend (port 4001) - Backend services
## Environment Variables
This module requires the following environment variables to be set in a `.env` file:
- `EMULATORJS_FRONTEND_PORT`: Host port for the main web interface
- `EMULATORJS_CONFIG_PORT`: Host port for the configuration interface
- `EMULATORJS_BACKEND_PORT`: Host port for backend services
## Data Persistence
EmulatorJS stores its data in two volumes:
1. Configuration: `/config` in the container, mapped to `${volume_path}/config` on the host
2. Game data: `/data` in the container, mapped to `${volume_path}/data` on the host
## Example Integration in Main Configuration
```hcl
module "emulatorjs" {
source = "./modules/20-services-apps/emulatorjs"
volume_path = module.system_globals.volume_host
}
# If you want to expose EmulatorJS via your networking modules,
# you may need to manually configure the service definition:
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.emulatorjs.service_definition,
# Other service definitions
]
}
```
## Additional Configuration
After deployment, you can access the configuration interface at `http://your-server:<config_port>` to:
1. Upload ROM files to the `/data/roms` directory
2. Configure emulation settings
3. Manage game art and metadata

View File

@@ -1,78 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the EmulatorJS container image"
type = string
default = "latest"
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
locals {
container_name = "emulatorjs"
image = "linuxserver/emulatorjs"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
env_file = "${path.module}/.env"
frontend_port = provider::dotenv::get_by_key("EMULATORJS_FRONTEND_PORT", local.env_file)
config_port = provider::dotenv::get_by_key("EMULATORJS_CONFIG_PORT", local.env_file)
backend_port = provider::dotenv::get_by_key("EMULATORJS_BACKEND_PORT", local.env_file)
ports = [
{
internal = 3000
external = local.config_port
protocol = "tcp"
},
{
internal = 80
external = local.frontend_port
protocol = "tcp"
},
{
internal = 4001
external = local.backend_port
protocol = "tcp"
}
]
volumes = [
{
host_path = "${var.volume_path}/config"
container_path = "/config"
read_only = false
},
{
host_path = "${var.volume_path}/data"
container_path = "/data"
read_only = false
}
]
}
module "emulatorjs" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.image_tag
volumes = local.volumes
ports = local.ports
monitoring = local.monitoring
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = module.emulatorjs.container_name
primary_port = local.frontend_port
endpoint = "http://${module.emulatorjs.container_name}:${local.frontend_port}"
}
}

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 @@
POSTGRES_USER=admin
POSTGRES_PASSWORD=
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=
POSTGRES_NON_ROOT_PASSWORD=
N8N_HOST=localhost
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,159 +0,0 @@
# 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.
## Overview
The n8n module:
- Deploys four Docker containers:
- `n8n`: The main workflow automation server
- `n8n-postgres`: A PostgreSQL database backend
- `n8n-redis`: A Redis instance for queuing
- `n8n-mcp`: A community node management tool for n8n
- Creates a dedicated Docker network (`n8n-network`) for container communication
- Persists data to volumes on the host
- Provides service definitions for integration with networking modules
## Usage
```hcl
module "n8n" {
source = "./modules/20-services-apps/n8n"
volume_path = "/path/to/volumes/n8n"
networks = ["homelab-network"]
}
```
## Variables
| 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` | - |
| `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 |
## Service Definitions
This module outputs two service definitions that are used by the networking modules to expose the services.
### n8n
```hcl
{
name = "n8n"
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"
}
```
## 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:
- **Database configuration (`n8n-postgres`)**:
- `POSTGRES_USER`: Root PostgreSQL user
- `POSTGRES_PASSWORD`: Root PostgreSQL password
- `POSTGRES_DB`: Database name for n8n
- `POSTGRES_NON_ROOT_USER`: Non-root user for n8n to connect with
- `POSTGRES_NON_ROOT_PASSWORD`: Password for the non-root user
- **n8n configuration (`n8n`)**:
- `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:
1. **n8n application data**: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
2. **PostgreSQL data**: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
3. **Redis data**: `/data` in the container, mapped to `${volume_path}/redis_data` on the host
4. **n8n-mcp data**: `/app/data` in the container, mapped to `${volume_path}/n8n_mcp_storage/_data` on the host
Additionally, an initialization script is mounted to the PostgreSQL container:
- `/docker-entrypoint-initdb.d/init-data.sh` in the container, from `${volume_path}/init-data.sh` on the host
## Networking
The module creates a dedicated Docker network named `n8n-network` for communication between all containers. The `n8n` and `n8n-mcp` containers are also attached to any additional networks specified in the `networks` variable, allowing them to communicate with other services in the homelab.
## Integration with Networking Modules
The services are configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
## Example Integration in Main Configuration
```hcl
module "n8n" {
source = "./modules/20-services-apps/n8n"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definitions are automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.n8n.service_definition,
module.n8n.n8n_mcp_service_definition,
# Other service definitions
]
}
```
## Using n8n-mcp with your IDE
To connect your IDE to the `n8n-mcp` server, you can use the following configuration in your IDE's settings. This allows the IDE to use the n8n instance as a tool provider.
Make sure to replace `<domain>` with your actual domain and populate the `AUTH_TOKEN` with the value of `N8N_MCP_AUTH_TOKEN` from your `.env` file.
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": [
"mcp-remote",
"https://n8n-mcp.<domain>/mcp",
"--header",
"Authorization: Bearer ${AUTH_TOKEN}",
"--transport",
"http-only"
],
"env": {
"AUTH_TOKEN": "..."
}
}
}
}
```

View File

@@ -1,13 +0,0 @@
#!/bin/bash
set -e;
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No Environment variables given!"
fi

View File

@@ -1,246 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the n8n 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 = []
}
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"
n8n_internal_port = 5678
# Define volumes
n8n_volumes = [
{
host_path = "${var.volume_path}/n8n_storage/_data"
container_path = "/home/node/.n8n"
read_only = false
}
]
database_volumes = [
{
host_path = "${var.volume_path}/db_storage/_data"
container_path = "/var/lib/postgresql/data"
read_only = false
},
{
host_path = "${var.volume_path}/init-data.sh"
container_path = "/docker-entrypoint-initdb.d/init-data.sh"
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_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"
}
# Healthcheck configuration for the database
database_healthcheck = {
test = ["CMD-SHELL", "pg_isready -h localhost -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval = "5s"
timeout = "5s"
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
module "postgres" {
source = "../../10-services-generic/docker-service"
container_name = local.database_name
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"
container_name = local.container_name
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]
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = local.n8n_internal_port
endpoint = "http://${local.container_name}:${local.n8n_internal_port}"
subdomains = ["n8n"]
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,81 +0,0 @@
# NTFY Module
This module deploys [NTFY](https://ntfy.sh/), a simple HTTP-based pub-sub notification service, as a Docker container in the homelab environment.
## Overview
The NTFY module:
- Deploys the `binwiederhier/ntfy` Docker container
- Persists configuration and cache data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "ntfy" {
source = "./modules/20-services-apps/ntfy"
volume_path = "/path/to/volumes/ntfy"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the NTFY image to use | `string` | `"latest"` |
| `volume_path` | Host path for NTFY data volumes | `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 = "ntfy"
primary_port = 80
endpoint = "http://ntfy:80"
subdomains = ["ntfy"]
publish_via = "reverse_proxy" # Expose via Caddy reverse proxy
proxied = true # Proxy through Cloudflare
}
```
## Data Persistence
NTFY stores its data in two volumes:
1. Configuration: `/etc/ntfy` in the container, mapped to `${volume_path}/app` on the host
2. Cache data: `/var/cache/ntfy` in the container, mapped to `${volume_path}/cache` 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"`. The `proxied = true` setting ensures that the DNS record is proxied through Cloudflare.
## Example Integration in Main Configuration
```hcl
module "ntfy" {
source = "./modules/20-services-apps/ntfy"
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.ntfy.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,58 +0,0 @@
variable "image_tag" {
description = "Tag of the ntfy image to use"
type = string
default = "latest"
}
variable "volume_path" {
description = "Host path for ntfy data volume"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
locals {
container_name = "ntfy"
image = "binwiederhier/ntfy"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
exposed_port = 80
subdomains = ["ntfy"]
default_volumes = [
{
container_path = "/etc/ntfy"
host_path = "${var.volume_path}/app"
read_only = false
},
{
container_path = "/var/cache/ntfy"
host_path = "${var.volume_path}/cache"
read_only = false
}
]
}
module "ntfy" {
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.exposed_port
endpoint = "http://${local.container_name}:${local.exposed_port}"
subdomains = local.subdomains
publish_via = "reverse_proxy"
proxied = true
}
}

View File

@@ -1,101 +0,0 @@
# Pterodactyl Module
This module is a parent module for deploying the [Pterodactyl](https://pterodactyl.io/) game server management system, which consists of multiple components:
1. **Panel** - The web-based administration interface and API server
2. **Wings** - The game server agent that controls individual game servers
## Overview
The Pterodactyl module consists of two submodules:
- `panel` - Deploys the Pterodactyl control panel with its database and cache servers
- `wings` - Deploys the Pterodactyl Wings agent for running game servers
For a complete installation, both components should be deployed.
## Architecture
Pterodactyl is designed with a client-server architecture:
- **Panel (Server)**: The central management interface where administrators create servers, manage users, and configure settings.
- **Wings (Agent)**: Installed on each machine that will run game servers, communicates with the Panel via API.
In a homelab environment, you might deploy both components on the same machine or separate them for better resource allocation.
## Usage
### Deploying Both Components
```hcl
module "pterodactyl_panel" {
source = "./modules/20-services-apps/pterodactyl/panel"
volume_path = "${var.volume_host}/pterodactyl/panel"
networks = [module.services.homelab_docker_network_name]
}
module "pterodactyl_wings" {
source = "./modules/20-services-apps/pterodactyl/wings"
volume_path = "${var.volume_host}/pterodactyl/wings"
networks = [module.services.homelab_docker_network_name]
}
# Include both service definitions in your networking modules
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.pterodactyl_panel.service_definition,
module.pterodactyl_wings.service_definition,
# Other service definitions
]
}
```
## Configuration Requirements
### Panel Setup
1. Create a `.env` file in the panel module directory with required variables:
- Database credentials (`MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD`, etc.)
- App settings (`APP_URL`, `APP_TIMEZONE`, etc.)
- CORS and proxy settings
2. SMTP settings are sourced from the global SMTP module
### Wings Setup
1. After deploying the Panel, you need to:
- Create a node in the Panel UI
- Download the wings configuration from the Panel
- Place it at `${volume_path}/etc/config.yml` for the Wings module
## Network Configuration
Both components create their own dedicated Docker networks:
- `ptero-panel`: For communication between Panel, database, and cache
- `ptero-wings`: For communication between Wings and game servers
Additionally, both components need to be connected to your main homelab network to communicate with each other.
## Service Definitions
Both components generate service definitions that can be used by your networking modules:
- Panel: Published on the domain `gameservers.yourdomain.com`
- Wings: Published on the domain `wings.yourdomain.com`
## Security Considerations
- Wings requires `privileged` mode to create game server containers
- Panel communicates with Wings via API using a token configured in the wings config.yml
## Additional Documentation
For more detailed information about each component, please see:
- [Panel README](/modules/20-services-apps/pterodactyl/panel/README.md)
- [Wings README](/modules/20-services-apps/pterodactyl/wings/README.md)
For official Pterodactyl documentation, visit [https://pterodactyl.io/](https://pterodactyl.io/)

View File

@@ -1,18 +0,0 @@
# Pterodactyl Panel Environment Settings
# Database Configuration
MYSQL_PASSWORD=secure_database_password_here
MYSQL_ROOT_PASSWORD=secure_root_password_here
MYSQL_DATABASE=pterodactyl
MYSQL_USER=pterodactyl
# Panel Configuration
APP_URL=https://panel.yourdomain.com
APP_TIMEZONE=Australia/Brisbane
APP_SERVICE_AUTHOR=email@example.com
APP_CORS_ALLOWED_ORIGINS=https://panel.yourdomain.com
TRUSTED_PROXIES="*" # Set this to your proxy IP
# Optional: Let's Encrypt Settings
# Uncomment and set to your email to use Let's Encrypt
# LE_EMAIL=admin@yourdomain.com

View File

@@ -1,109 +0,0 @@
# Pterodactyl Panel Module
This module deploys [Pterodactyl Panel](https://pterodactyl.io/), a game server management panel, as Docker containers in the homelab environment.
## Overview
The Pterodactyl Panel module:
- Deploys three Docker containers:
- `pterodactyl-panel`: The main web UI and API server
- `pterodactyl-db`: A MariaDB database backend
- `pterodactyl-cache`: A Redis cache server
- Creates a dedicated Docker network (`ptero-panel`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "pterodactyl_panel" {
source = "./modules/20-services-apps/pterodactyl/panel"
volume_path = "/path/to/volumes/pterodactyl/panel"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the Pterodactyl Panel image to use | `string` | `"latest"` |
| `volume_path` | Host path for Pterodactyl Panel volumes | `string` | - |
| `networks` | List of networks to which the panel 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 = "pterodactyl-panel"
primary_port = 80
endpoint = "http://pterodactyl-panel:80"
subdomains = ["gameservers"]
publish_via = "tunnel"
}
```
## Environment Variables
Pterodactyl Panel 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. Key variables include:
- Panel Configuration:
- `APP_URL`: The URL where the panel will be accessed
- `APP_TIMEZONE`: The timezone for the application
- `APP_SERVICE_AUTHOR`: Service author information
- Database Configuration:
- `MYSQL_PASSWORD`: Database password
- `MYSQL_ROOT_PASSWORD`: Database root password
- `MYSQL_DATABASE`: Database name
- `MYSQL_USER`: Database username
- Mail Configuration:
- Mail settings are automatically sourced from the global SMTP module
## Data Persistence
Pterodactyl Panel stores its data in multiple volumes:
1. Application data: `/app/var` in the container, mapped to `${volume_path}/var` on the host
2. Nginx configuration: `/etc/nginx/http.d` in the container, mapped to `${volume_path}/nginx` on the host
3. SSL certificates: `/etc/letsencrypt` in the container, mapped to `${volume_path}/certs` on the host
4. Logs: `/app/storage/logs` in the container, mapped to `${volume_path}/logs` on the host
5. Database data: `/var/lib/mysql` in the MariaDB container, mapped to `${volume_path}/database` on the host
## Networking
The module creates a dedicated Docker network named `ptero-panel` for communication between the panel, database, and cache containers. The panel 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
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 "pterodactyl_panel" {
source = "./modules/20-services-apps/pterodactyl/panel"
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.pterodactyl_panel.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,164 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
module "smtp" {
source = "../../../00-globals/smtp"
}
variable "image_tag" {
description = "The tag for the Pterodactyl Panel 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 = "pterodactyl-panel"
database_name = "pterodactyl-db"
cache_name = "pterodactyl-cache"
panel_image = "ghcr.io/pterodactyl/panel"
database_image = "mariadb"
cache_image = "redis"
panel_tag = var.image_tag != "" ? var.image_tag : "latest"
database_tag = "10.5"
cache_tag = "alpine"
monitoring = true
env_file = "${path.module}/.env"
# Volume paths
panel_volumes = [
{
host_path = "${var.volume_path}/var"
container_path = "/app/var"
read_only = false
},
{
host_path = "${var.volume_path}/nginx"
container_path = "/etc/nginx/http.d"
read_only = false
},
{
host_path = "${var.volume_path}/certs"
container_path = "/etc/letsencrypt"
read_only = false
},
{
host_path = "${var.volume_path}/logs"
container_path = "/app/storage/logs"
read_only = false
}
]
database_volumes = [
{
host_path = "${var.volume_path}/database"
container_path = "/var/lib/mysql"
read_only = false
}
]
# Environment variables
panel_env_vars = {
APP_URL = provider::dotenv::get_by_key("APP_URL", local.env_file)
APP_TIMEZONE = provider::dotenv::get_by_key("APP_TIMEZONE", local.env_file)
APP_SERVICE_AUTHOR = provider::dotenv::get_by_key("APP_SERVICE_AUTHOR", local.env_file)
APP_CORS_ALLOWED_ORIGINS = provider::dotenv::get_by_key("APP_CORS_ALLOWED_ORIGINS", local.env_file)
TRUSTED_PROXIES = provider::dotenv::get_by_key("TRUSTED_PROXIES", local.env_file)
MAIL_FROM = module.smtp.mail_from
MAIL_DRIVER = "smtp"
MAIL_HOST = module.smtp.mail_host
MAIL_PORT = module.smtp.mail_port
MAIL_USERNAME = module.smtp.mail_username
MAIL_PASSWORD = module.smtp.mail_password
MAIL_ENCRYPTION = "false"
DB_PASSWORD = provider::dotenv::get_by_key("MYSQL_PASSWORD", local.env_file)
APP_ENV = "production"
APP_ENVIRONMENT_ONLY = "false"
CACHE_DRIVER = "redis"
SESSION_DRIVER = "redis"
QUEUE_DRIVER = "redis"
REDIS_HOST = local.cache_name
DB_HOST = local.database_name
DB_PORT = "3306"
DB_DATABASE = provider::dotenv::get_by_key("MYSQL_DATABASE", local.env_file)
DB_USERNAME = provider::dotenv::get_by_key("MYSQL_USER", local.env_file)
}
database_env_vars = {
MYSQL_PASSWORD = provider::dotenv::get_by_key("MYSQL_PASSWORD", local.env_file)
MYSQL_ROOT_PASSWORD = provider::dotenv::get_by_key("MYSQL_ROOT_PASSWORD", local.env_file)
MYSQL_DATABASE = provider::dotenv::get_by_key("MYSQL_DATABASE", local.env_file)
MYSQL_USER = provider::dotenv::get_by_key("MYSQL_USER", local.env_file)
}
}
# Create a dedicated network for Pterodactyl
module "pterodactyl_network" {
source = "../../../01-networking/docker-network"
name = "ptero-panel"
driver = "bridge"
subnet = "172.20.0.0/16"
attachable = true
}
# Database container
module "database" {
source = "../../../10-services-generic/docker-service"
container_name = local.database_name
image = local.database_image
tag = local.database_tag
volumes = local.database_volumes
env_vars = local.database_env_vars
networks = [module.pterodactyl_network.name]
command = ["--default-authentication-plugin=mysql_native_password"]
monitoring = local.monitoring
}
# Cache container
module "cache" {
source = "../../../10-services-generic/docker-service"
container_name = local.cache_name
image = local.cache_image
tag = local.cache_tag
networks = [module.pterodactyl_network.name]
monitoring = local.monitoring
}
# Panel container
module "panel" {
source = "../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.panel_image
tag = local.panel_tag
volumes = local.panel_volumes
env_vars = local.panel_env_vars
networks = concat([module.pterodactyl_network.name], var.networks)
monitoring = local.monitoring
depends_on = [module.database, module.cache]
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = 80
endpoint = "http://${local.container_name}:80"
subdomains = ["gameservers"]
publish_via = "tunnel"
}
}

View File

@@ -1,100 +0,0 @@
# Pterodactyl Wings Module
This module deploys [Pterodactyl Wings](https://pterodactyl.io/wings/), the game server agent component of Pterodactyl, as a Docker container in the homelab environment.
## Overview
The Pterodactyl Wings module:
- Deploys the `pterodactyl-wings` Docker container
- Creates a dedicated Docker network (`ptero-wings`) for game server communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
- Runs with privileged mode to manage game server containers
## Usage
```hcl
module "pterodactyl_wings" {
source = "./modules/20-services-apps/pterodactyl/wings"
volume_path = "/path/to/volumes/pterodactyl/wings"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ------------------------------------------------------- | -------------- | ----------- |
| `image_tag` | Tag of the Pterodactyl Wings image to use | `string` | `"v1.11.3"` |
| `volume_path` | Host path for Pterodactyl Wings volumes | `string` | - |
| `networks` | List of networks to which wings 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 = "pterodactyl-wings"
primary_port = 443
endpoint = "http://pterodactyl-wings:443"
subdomains = ["wings"]
publish_via = "tunnel"
}
```
## Environment Variables
Pterodactyl Wings uses the following environment variables:
- `TZ`: Timezone (set to Australia/Brisbane)
- `WINGS_UID`: User ID for wings process (988)
- `WINGS_GID`: Group ID for wings process (988)
- `WINGS_USERNAME`: Username for wings process ("pterodactyl")
## Data Persistence
Pterodactyl Wings uses several volume mounts:
1. Docker socket: `/var/run/docker.sock` (for controlling game server containers)
2. Docker containers: `/var/lib/docker/containers/` (for accessing container information)
3. SSL certificates: `/etc/ssl/certs` (mounted read-only)
4. Wings configuration: `/etc/pterodactyl/` in the container, mapped to `${volume_path}/etc`
5. Wings data: `/var/lib` in the container, mapped to `${volume_path}/var/lib`
6. Logs: `/var/log/pterodactyl/` in the container, mapped to `${volume_path}/var/log`
7. Temporary files: `${volume_path}/tmp` in the container and host
## Networking
The module creates a dedicated Docker network named `ptero-wings` for game server communication. This network is configured with the subnet `172.21.0.0/16` and is made attachable to allow game server containers to connect to it. The wings container is also attached to any additional networks specified in the `networks` variable.
## 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 "pterodactyl_wings" {
source = "./modules/20-services-apps/pterodactyl/wings"
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.pterodactyl_wings.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,118 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the Pterodactyl Wings container image"
type = string
default = "v1.11.3"
}
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 = "pterodactyl-wings"
image = "ghcr.io/pterodactyl/wings"
image_tag = var.image_tag != "" ? var.image_tag : "v1.11.3"
monitoring = false
env_file = "${path.module}/.env"
subdomains = ["wings"]
# Volumes configuration
volumes = [
{
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
read_only = false
},
{
host_path = "/var/lib/docker/containers/"
container_path = "/var/lib/docker/containers/"
read_only = false
},
{
host_path = "/etc/ssl/certs"
container_path = "/etc/ssl/certs"
read_only = true
},
{
host_path = "${var.volume_path}/etc"
container_path = "/etc/pterodactyl/"
read_only = false
},
{
host_path = "${var.volume_path}/var/lib"
container_path = "${var.volume_path}/var/lib"
read_only = false
},
{
host_path = "${var.volume_path}/var/log"
container_path = "/var/log/pterodactyl/"
read_only = false
},
{
host_path = "${var.volume_path}/tmp"
container_path = "${var.volume_path}/tmp"
read_only = false
},
]
# Environment variables
env_vars = {
TZ = "Australia/Brisbane"
WINGS_UID = 988
WINGS_GID = 988
WINGS_USERNAME = "pterodactyl"
}
}
# Create a custom Docker network for wings
module "wings_network" {
source = "../../../01-networking/docker-network"
name = "ptero-wings"
driver = "bridge"
attachable = true
subnet = "172.21.0.0/16"
options = {
"com.docker.network.bridge.name" = "ptero-wings"
}
}
module "wings" {
source = "../../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.image_tag
pgid = 988
puid = 988
volumes = local.volumes
env_vars = local.env_vars
networks = concat([module.wings_network.name], var.networks)
monitoring = local.monitoring
privileged = true
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = 443
endpoint = "http://${local.container_name}:443"
subdomains = local.subdomains
publish_via = "tunnel"
}
}

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

View File

@@ -1,79 +0,0 @@
# SearxNG Module
This module deploys [SearxNG](https://searx.github.io/searx/), a privacy-respecting metasearch engine, as a Docker container in the homelab environment.
## Overview
The SearxNG module:
- Deploys the `searxng/searxng` Docker container
- Persists configuration data to a volume on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "searxng" {
source = "./modules/20-services-apps/searxng"
volume_path = "/path/to/volumes/searxng"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the SearxNG image to use | `string` | `"latest"` |
| `volume_path` | Host path for SearxNG configuration 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 = "searxng"
primary_port = 8080
endpoint = "http://searxng:8080"
subdomains = ["search"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Data Persistence
SearxNG stores its configuration in a single volume:
- Configuration: `/etc/searxng` in the container, mapped to `${volume_path}/config` on the host
## 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 "searxng" {
source = "./modules/20-services-apps/searxng"
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.searxng.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,60 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the searxng 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 = "searxng"
image = "searxng/searxng"
tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
internal_port = 8080
volumes = [
{
host_path = "${var.volume_path}/config"
container_path = "/etc/searxng"
read_only = false
}
]
}
module "searxng" {
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
}
output "service_definition" {
description = "Service definition with ingress configuration"
value = {
name = local.container_name
primary_port = local.internal_port
endpoint = "http://${local.container_name}:${local.internal_port}"
subdomains = ["search"]
publish_via = "tunnel"
}
}

View File

@@ -30,72 +30,18 @@ module "media_docker_network" {
subnet = "10.110.0.0/16" subnet = "10.110.0.0/16"
} }
module "actualbudget" {
source = "${local.module_dir}/20-services-apps/actualbudget"
volume_path = "${local.volume_host}/actual"
networks = [module.homelab_docker_network.name]
}
module "affine" {
source = "${local.module_dir}/20-services-apps/affine"
volume_path = "${local.volume_host}/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" { module "calibre" {
source = "${local.module_dir}/20-services-apps/calibre" source = "${local.module_dir}/20-services-apps/calibre"
volume_path = "${local.volume_host}/calibre" volume_path = "${local.volume_host}/calibre"
networks = [module.homelab_docker_network.name] 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" { module "glance" {
source = "${local.module_dir}/20-services-apps/glance" source = "${local.module_dir}/20-services-apps/glance"
volume_path = "${local.volume_host}/glance" volume_path = "${local.volume_host}/glance"
networks = [module.homelab_docker_network.name] 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" { module "immich" {
source = "${local.module_dir}/20-services-apps/immich" source = "${local.module_dir}/20-services-apps/immich"
@@ -117,23 +63,6 @@ module "linkwarden" {
networks = [module.homelab_docker_network.name] 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" { module "portainer" {
source = "${local.module_dir}/20-services-apps/portainer" source = "${local.module_dir}/20-services-apps/portainer"
@@ -141,37 +70,3 @@ module "portainer" {
networks = [module.homelab_docker_network.name] 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"
networks = [module.homelab_docker_network.name]
}
module "pterodactyl_wings" {
source = "${local.module_dir}/20-services-apps/pterodactyl/wings"
volume_path = "${local.volume_host}/pterodactyl/wings"
networks = [module.homelab_docker_network.name]
}
module "qbittorrent" {
source = "${local.module_dir}/20-services-apps/qbittorrent"
volume_path = "${local.volume_host}/qbittorrent"
downloads_path = "${local.data_host}/torrents"
networks = [module.media_docker_network.name]
connect_via_gluetun = true
gluetun_container_name = "gluetun"
depends_on = [module.gluetun]
}
module "sabnzbd" {
source = "${local.module_dir}/20-services-apps/sabnzbd"
volume_path = "${local.volume_host}/sabnzbd"
downloads_path = "${local.data_host}/usenet/downloads"
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
}
module "searxng" {
source = "${local.module_dir}/20-services-apps/searxng"
volume_path = "${local.volume_host}/searxng"
networks = [module.homelab_docker_network.name]
}

View File

@@ -4,27 +4,12 @@
output "service_definitions" { output "service_definitions" {
description = "Service definitions for all services" description = "Service definitions for all services"
value = [ value = [
module.actualbudget.service_definition,
module.affine.service_definition,
module.arr.service_definition,
module.calibre.service_definition, module.calibre.service_definition,
module.copyparty.service_definition,
module.crawl4ai.service_definition,
module.emulatorjs.service_definition,
module.glance.service_definition, module.glance.service_definition,
module.immich.service_definition, module.immich.service_definition,
module.jellyfin.service_definition, module.jellyfin.service_definition,
module.linkwarden.service_definition, module.linkwarden.service_definition,
module.n8n.service_definition, module.portainer.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.searxng.service_definition
] ]
} }