Pruning
This commit is contained in:
@@ -101,14 +101,14 @@ The `publish_via` field controls which networking module(s) will expose the serv
|
||||
### Basic Service with Default Settings
|
||||
|
||||
```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" {
|
||||
description = "Service definition for a notification service"
|
||||
description = "Service definition for a media server"
|
||||
value = {
|
||||
name = "ntfy"
|
||||
primary_port = 80
|
||||
endpoint = "http://ntfy:80"
|
||||
subdomains = ["ntfy"]
|
||||
name = "jellyfin"
|
||||
primary_port = 8096
|
||||
endpoint = "http://jellyfin:8096"
|
||||
subdomains = ["media"]
|
||||
publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy
|
||||
proxied = false # Don't proxy through Cloudflare (expose direct IP)
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ module "homelab_tunnel" {
|
||||
tunnel_name = "homelab-tunnel"
|
||||
ingress_rules = [
|
||||
{
|
||||
hostname = "budget.${module.cloudflare_globals.domain}"
|
||||
service = "http://actualbudget:5006"
|
||||
hostname = "media.${module.cloudflare_globals.domain}"
|
||||
service = "http://jellyfin:8096"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
EMULATORJS_FRONTEND_PORT=5823
|
||||
EMULATORJS_CONFIG_PORT=5824
|
||||
EMULATORJS_BACKEND_PORT=5825
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
@@ -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=
|
||||
@@ -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": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Database Configuration
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=change_this_password
|
||||
DB_DATABASE=root_db
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user