23 Commits

Author SHA1 Message Date
e52d667cab Remove calibre 2025-10-03 15:50:04 +01:00
0fe34fb0e4 Pruning 2025-10-03 15:49:36 +01:00
Yuris Cakranegara
bce43c4a71 feat: add media server 2025-08-21 17:42:48 +10:00
Yuris Cakranegara
60e3a41ac5 feat(immich): proxy through cloudflare 2025-08-21 17:41:55 +10:00
Yuris Cakranegara
b9301fff36 feat(calibre): proxy through cloudflare 2025-08-21 17:41:48 +10:00
Yuris Cakranegara
80f8857dd2 feat(affine): proxy through cloudflare 2025-08-21 17:41:39 +10:00
Yuris Cakranegara
2c8c43ff68 feat(docker-service): allow adding group, capabilities, and device mappings 2025-08-21 17:41:10 +10:00
Yuris Cakranegara
4edfd642f3 fix(caddy): remove unused volume 2025-08-21 17:40:06 +10:00
Yuris Cakranegara
c59ebbcc8b style: tf formatting 2025-08-17 22:25:55 +10:00
Yuris Cakranegara
8ee71193bb feat: add immich 2025-08-17 22:24:47 +10:00
Yuris Cakranegara
4f5ee19cef feat(caddy): port mappings to standard 80/443 2025-08-17 20:47:42 +10:00
Yuris Cakranegara
9c46aa0d5b feat: add copyparty 2025-08-06 11:08:22 +10:00
Yuris Cakranegara
0a076a9af5 feat(docker-service): add shutdown grace period 2025-08-06 11:08:05 +10:00
Yuris Cakranegara
6595de4788 feat: restructure volume path 2025-08-06 11:07:18 +10:00
Yuris Cakranegara
3725c73bce feat(emulatorjs): define specific image version 2025-08-06 01:56:44 +10:00
Yuris Cakranegara
eefe369975 feat: add portainer 2025-08-06 01:56:00 +10:00
Yuris Cakranegara
ede6c52a40 feat(n8n): add n8n mcp server 2025-07-06 16:34:24 +10:00
Yuris Cakranegara
a63f144bf1 feat: add crawl4ai 2025-06-30 22:22:08 +10:00
Yuris Cakranegara
82d8ca0463 fix(nocodb): volume path 2025-06-30 22:21:55 +10:00
Yuris Cakranegara
c02ac6f961 feat: add nocodb 2025-06-30 09:45:47 +10:00
Yuris Cakranegara
b03034b742 feat(n8n): add redis 2025-06-29 04:08:07 +10:00
Yuris Cakranegara
26808e4ca6 feat(n8n): define private network subnet 2025-06-28 13:05:24 +10:00
Yuris Cakranegara
5cd8d36d97 feat: add glance 2025-06-28 13:04:30 +10:00
56 changed files with 767 additions and 3285 deletions

View File

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

View File

@@ -19,6 +19,10 @@ module "services" {
source = "./services"
}
locals {
volume_host = "${module.system_globals.volume_host}/appdata"
}
module "homelab_cloudflared_tunnel" {
source = "./modules/01-networking/cloudflared-tunnel"
cloudflare_account_id = module.cloudflare_globals.cloudflare_account_id
@@ -40,7 +44,7 @@ module "homelab_caddy_proxy" {
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
external_ip = module.cloudflare_globals.external_ip
service_definitions = module.services.service_definitions
volume_path = module.system_globals.volume_host
volume_path = local.volume_host
networks = [module.services.homelab_docker_network_name]
monitoring = true
}

View File

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

View File

@@ -63,10 +63,6 @@ locals {
])
}
resource "docker_volume" "caddy_config" {
name = "${local.container_name}_config"
}
// Create Caddyfile in the volume path
resource "local_file" "caddyfile" {
content = local.caddyfile_content
@@ -115,12 +111,12 @@ module "caddy" {
ports = [
{
external = "9080"
external = "80"
internal = "80"
protocol = "tcp"
},
{
external = "9443"
external = "443"
internal = "443"
protocol = "tcp"
}

View File

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

View File

@@ -73,7 +73,7 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "this" {
}
module "dns_records" {
source = "../../10-services-generic/cloudflare-dns"
source = "../../10-services-generic/cloudflare-dns"
zone_id = var.cloudflare_zone_id
hostnames = [
for rule in local.all_ingress_rules :

View File

@@ -71,7 +71,7 @@ resource "docker_container" "service_container" {
# Set the network mode (bridge, host, etc.)
network_mode = local.network_mode
# Add host mappings (entries for /etc/hosts)
dynamic "host" {
for_each = var.host_mappings
@@ -139,15 +139,36 @@ resource "docker_container" "service_container" {
cpu_shares = var.cpu_shares
# Other container options
dns = var.dns
dns_search = var.dns_search
hostname = var.hostname
domainname = var.domainname
user = var.user
working_dir = var.working_dir
command = var.command
entrypoint = var.entrypoint
privileged = var.privileged
dns = var.dns
dns_search = var.dns_search
hostname = var.hostname
domainname = var.domainname
user = var.user
group_add = var.group_add
working_dir = var.working_dir
command = var.command
entrypoint = var.entrypoint
privileged = var.privileged
destroy_grace_seconds = var.destroy_grace_seconds
# Linux capabilities controls
dynamic "capabilities" {
for_each = length(var.capabilities_add) > 0 || length(var.capabilities_drop) > 0 ? [1] : []
content {
add = var.capabilities_add
drop = var.capabilities_drop
}
}
# Device mappings
dynamic "devices" {
for_each = var.devices
content {
host_path = devices.value.host_path
container_path = devices.value.container_path
permissions = devices.value.permissions
}
}
# Set log options
log_driver = var.log_driver

View File

@@ -179,12 +179,48 @@ variable "entrypoint" {
default = null
}
variable "group_add" {
description = "Additional groups to add to the container"
type = list(string)
default = []
}
variable "privileged" {
description = "Run container in privileged mode"
type = bool
default = false
}
// Linux capabilities controls
variable "capabilities_add" {
description = "Linux capabilities to add to the container"
type = list(string)
default = []
}
variable "capabilities_drop" {
description = "Linux capabilities to drop from the container"
type = list(string)
default = []
}
// Devices to pass through to container
variable "devices" {
description = "List of device mappings for the container"
type = list(object({
host_path = string
container_path = string
permissions = string
}))
default = []
}
variable "destroy_grace_seconds" {
description = "Grace period in seconds before the container is destroyed"
type = number
default = 10
}
// Logging options
variable "log_driver" {
description = "Log driver for the container"
@@ -195,8 +231,8 @@ variable "log_driver" {
variable "log_opts" {
description = "Log driver options"
type = map(string)
default = {
max-size = "10m"
max-file = "3"
default = {
max-size = "10m"
max-file = "3"
}
}

View File

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

View File

@@ -1,121 +0,0 @@
# AFFiNE Module
This module deploys [AFFiNE](https://affine.pro/), a privacy-first, local-first, note-taking and knowledge base application, as Docker containers in the homelab environment.
## Overview
The AFFiNE module:
- Deploys four Docker containers:
- `affine_server`: The main AFFiNE application server
- `affine_migration_job`: A container that runs pre-deployment migrations
- `affine_postgres`: A PostgreSQL (pgvector) database backend
- `affine_redis`: A Redis instance for caching and temporary data
- Creates a dedicated Docker network (`affine-network`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "affine" {
source = "./modules/20-services-apps/affine"
volume_path = "/path/to/volumes/affine"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | -------------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the AFFiNE image to use | `string` | `"stable"` |
| `volume_path` | Host path for AFFiNE and database data volumes | `string` | - |
| `networks` | List of additional networks to which AFFiNE should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "affine_server"
primary_port = 3010
endpoint = "http://affine_server:3010"
subdomains = ["affine"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Environment Variables
AFFiNE requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
- Database configuration:
- `DB_USERNAME`: PostgreSQL user
- `DB_PASSWORD`: PostgreSQL password
- `DB_DATABASE`: Database name (defaults to "affine")
- AFFiNE configuration:
- `AFFINE_REVISION`: Version of AFFiNE to use ("stable" or "canary") (defaults to "canary")
- `PORT`: External port for the AFFiNE server (defaults to 3010)
- `AFFINE_SERVER_HTTPS`: Whether to use HTTPS (defaults to "true")
- `AFFINE_SERVER_HOST`: Hostname for the AFFiNE server
- `AFFINE_SERVER_NAME`: Name for the AFFiNE server (defaults to "AFFiNE Selfhosted")
- Cloudflare R2 configuration:
- `R2_OBJECT_STORAGE_ACCOUNT_ID`: Cloudflare R2 account ID
- `R2_OBJECT_STORAGE_ACCESS_KEY_ID`: Cloudflare R2 access key ID
- `R2_OBJECT_STORAGE_SECRET_ACCESS_KEY`: Cloudflare R2 secret access key
## Data Persistence
AFFiNE stores its data in three main volumes:
1. AFFiNE application data: `/root/.affine/storage` in the container, mapped to `${volume_path}/self-host/storage` on the host
2. AFFiNE configuration: `/root/.affine/config` in the container, mapped to `${volume_path}/self-host/config` on the host
3. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/self-host/postgres/pgdata` on the host
## Networking
The module creates a dedicated Docker network named `affine-network` for communication between the AFFiNE components. The AFFiNE server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## Dependencies
The AFFiNE containers have the following dependencies:
- The main `affine_server` depends on PostgreSQL, Redis, and the migration job
- The migration job depends on PostgreSQL and Redis
- Both PostgreSQL and Redis use healthchecks to ensure they're ready before dependent services start
## Integration with Networking Modules
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
## Example Integration in Main Configuration
```hcl
module "affine" {
source = "./modules/20-services-apps/affine"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.affine.service_definition,
# Other service definitions
]
}
```

View File

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

View File

@@ -1,102 +0,0 @@
# Calibre Module
This module deploys [Calibre Web Automated](https://hub.docker.com/r/crocodilestick/calibre-web-automated), a web app for browsing, reading, and managing eBooks, as a Docker container in the homelab environment.
## Overview
The Calibre module:
- Deploys a Docker container:
- `calibre-web-automated`: The main Calibre Web application with automation features
- Creates a dedicated Docker network (`calibre-network`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "calibre" {
source = "./modules/20-services-apps/calibre"
volume_path = "/path/to/volumes"
networks = ["homelab-network"]
user_id = "1000"
group_id = "1000"
timezone = "UTC"
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | --------------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the Calibre Web image to use | `string` | `"latest"` |
| `volume_path` | Host path for Calibre data volumes | `string` | - |
| `networks` | List of additional networks to which Calibre should be attached | `list(string)` | `[]` |
| `user_id` | User ID for container permissions | `string` | `"1000"` |
| `group_id` | Group ID for container permissions | `string` | `"1000"` |
| `timezone` | Timezone for the container | `string` | `"UTC"` |
## 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 = "calibre-web-automated"
primary_port = 8083
endpoint = "http://calibre-web-automated:8083"
subdomains = ["calibre"]
publish_via = "reverse_proxy"
proxied = false
}
```
## Docker Mods
This module includes the Calibre Docker mod to add Calibre functionality to the container:
- `lscr.io/linuxserver/mods:universal-calibre-v7.16.0`
## Data Persistence
Calibre stores its data in three main volumes:
1. Configuration data: `/config` in the container, mapped to `${volume_path}/config` on the host
2. Book ingest directory: `/cwa-book-ingest` in the container, mapped to `${volume_path}/ingest` on the host
3. Calibre library: `/calibre-library` in the container, mapped to `${volume_path}/library` on the host
## Networking
The Calibre container is 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 the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
## Example Integration in Main Configuration
```hcl
module "calibre" {
source = "./modules/20-services-apps/calibre"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
user_id = module.system_globals.user_id
group_id = module.system_globals.group_id
timezone = module.system_globals.timezone
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.calibre.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,104 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the Calibre Web 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 = []
}
variable "user_id" {
description = "User ID for container permissions"
type = string
default = "1000"
}
variable "group_id" {
description = "Group ID for container permissions"
type = string
default = "1000"
}
variable "timezone" {
description = "Timezone for the container"
type = string
default = "UTC"
}
locals {
container_name = "calibre-web-automated"
calibre_image = "crocodilestick/calibre-web-automated"
calibre_tag = var.image_tag
monitoring = true
env_file = "${path.module}/.env"
calibre_internal_port = 8083
docker_mods = "lscr.io/linuxserver/mods:universal-calibre-v7.16.0"
# Define volumes
calibre_volumes = [
{
host_path = "${var.volume_path}/config"
container_path = "/config"
read_only = false
},
{
host_path = "${var.volume_path}/ingest"
container_path = "/cwa-book-ingest"
read_only = false
},
{
host_path = "${var.volume_path}/library"
container_path = "/calibre-library"
read_only = false
}
]
# Environment variables for Calibre Web
calibre_env_vars = {
PUID = var.user_id
PGID = var.group_id
TZ = var.timezone
DOCKER_MODS = local.docker_mods
}
}
# Create the Calibre Web container
module "calibre" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.calibre_image
tag = local.calibre_tag
volumes = local.calibre_volumes
env_vars = local.calibre_env_vars
networks = concat(var.networks)
monitoring = local.monitoring
restart_policy = "always"
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = local.calibre_internal_port
endpoint = "http://${local.container_name}:${local.calibre_internal_port}"
subdomains = ["calibre"]
publish_via = "reverse_proxy"
proxied = false
}
}

View File

@@ -1,3 +0,0 @@
EMULATORJS_FRONTEND_PORT=5823
EMULATORJS_CONFIG_PORT=5824
EMULATORJS_BACKEND_PORT=5825

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
# ActualBudget Module
# Glance Module
This module deploys [ActualBudget](https://actualbudget.com/), a personal finance and budgeting application, as a Docker container in the homelab environment.
This module deploys [Glance](https://glanceapp.io/), a dashboard application, as a Docker container in the homelab environment.
## Overview
The ActualBudget module:
The Glance module:
- Deploys the `actualbudget/actual-server` Docker container
- Persists data to a volume on the host
- Deploys the `glanceapp/glance` Docker container
- Persists configuration to a volume on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "actualbudget" {
source = "./modules/20-services-apps/actualbudget"
volume_path = "/path/to/volumes/actualbudget"
module "glance" {
source = "./modules/20-services-apps/glance"
volume_path = "/path/to/volumes/glance"
networks = ["homelab-network"]
}
```
@@ -24,8 +24,8 @@ module "actualbudget" {
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the ActualBudget image to use | `string` | `"latest"` |
| `volume_path` | Host path for ActualBudget data volume | `string` | - |
| `image_tag` | Tag of the Glance image to use | `string` | `"latest"` |
| `volume_path` | Host path for Glance data volume | `string` | - |
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
## Outputs
@@ -40,17 +40,17 @@ This module outputs a service definition that is used by the networking modules
```hcl
{
name = "actualbudget"
primary_port = 5006
endpoint = "http://actualbudget:5006"
subdomains = ["budget"]
name = "glance"
primary_port = 4921
endpoint = "http://glance:4921"
subdomains = ["glance"]
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`.
Glance stores its configuration in the `/app/config` directory inside the container. This is mapped to a volume on the host at `${volume_path}/config`.
## Integration with Networking Modules
@@ -59,8 +59,8 @@ This service is configured to be exposed through a Cloudflare tunnel for secure
## Example Integration in Main Configuration
```hcl
module "actualbudget" {
source = "./modules/20-services-apps/actualbudget"
module "glance" {
source = "./modules/20-services-apps/glance"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
@@ -70,7 +70,7 @@ module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.actualbudget.service_definition,
module.glance.service_definition,
# Other service definitions
]
}

View File

@@ -1,11 +1,11 @@
variable "image_tag" {
description = "Tag of the ActualBudget image to use"
description = "Tag of the Glance image to use"
type = string
default = "latest"
}
variable "volume_path" {
description = "Host path for ActualBudget data volume"
description = "Host path for Glance data volume"
type = string
}
@@ -15,22 +15,22 @@ variable "networks" {
}
locals {
container_name = "actualbudget"
image = "actualbudget/actual-server"
container_name = "glance"
image = "glanceapp/glance"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
exposed_port = 5006
subdomains = ["budget"]
host_port = 8080
subdomains = ["glance"]
default_volumes = [
{
container_path = "/data"
host_path = "${var.volume_path}/data"
container_path = "/app/config"
host_path = "${var.volume_path}/config"
read_only = false
}
},
]
}
module "actualbudget" {
module "glance" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
@@ -44,9 +44,9 @@ 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"
primary_port = local.host_port
endpoint = "http://${local.container_name}:${local.host_port}"
subdomains = local.subdomains
publish_via = "tunnel"
}
}

View File

@@ -0,0 +1,14 @@
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
###################################################################################
# Required database configuration (used by Terraform to configure Postgres & Immich)
###################################################################################
# PostgreSQL username
DB_USERNAME=postgres
# PostgreSQL password (use only A-Za-z0-9 characters)
DB_PASSWORD=postgres
# PostgreSQL database name
DB_DATABASE_NAME=immich

View File

@@ -0,0 +1,110 @@
# Immich Module
This module deploys [Immich](https://immich.app/), a high-performance self-hosted photo and video backup solution, as Docker containers in the homelab environment.
## Overview
The Immich module:
- Deploys four Docker containers:
- `immich-server`: The main Immich API/UI server (port 2283)
- `immich-machine-learning`: The ML service for search, faces, and embeddings
- `immich-postgres`: Immich-tuned PostgreSQL database
- `immich-redis`: Valkey/Redis-compatible cache
- Creates a dedicated Docker network (`immich-network`) for inter-container communication
- Persists data to volumes on the host
- Provides a service definition for integration with networking modules
## Usage
```hcl
module "immich" {
source = "./modules/20-services-apps/immich"
appdata_path = "/path/to/appdata/immich"
library_path = "/path/to/data/media/photos"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| --------------- | -------------------------------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the Immich images to use (`server` and `machine-learning`) | `string` | `"release"` |
| `appdata_path` | Base host path for Immich app data (e.g., PostgreSQL data and internal configs) | `string` | - |
| `library_path` | Base host path for user library data and ML cache | `string` | - |
| `networks` | List of additional networks to which the server should attach | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition used by networking modules to expose the service.
```hcl
{
name = "immich-server"
primary_port = 2283
endpoint = "http://immich-server:2283"
subdomains = ["photos"]
publish_via = "reverse_proxy"
}
```
## Environment Variables
Only the database credentials are expected in a `.env` file in this module directory and are read using the `dotenv` Terraform provider. Everything else is configured directly in Terraform.
Required in `modules/20-services-apps/immich/.env`:
- `DB_USERNAME`: PostgreSQL user
- `DB_PASSWORD`: PostgreSQL password
- `DB_DATABASE_NAME`: Database name
A ready-to-copy `modules/20-services-apps/immich/.env.example` is provided.
## Data Persistence
Immich stores data in the following volumes:
1. Library storage: `/data` in `immich-server`, mapped to `${library_path}/library` on the host
2. ML model cache: `/cache` in `immich-machine-learning`, mapped to `${library_path}/machine-learning/cache` on the host
3. PostgreSQL data: `/var/lib/postgresql/data` in `immich-postgres`, mapped to `${appdata_path}/postgres/pgdata` on the host
## Networking
The module creates a dedicated Docker network named `immich-network` for communication between Immich components. The Immich server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## Dependencies
- `immich-server` depends on `immich-postgres` and `immich-redis`
- `immich-postgres` and `immich-redis` include healthchecks
- The ML service is independent and discovered by the server internally; tuning can be done via the Immich admin UI
## Integration with Networking Modules
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
## Example Integration in Main Configuration
```hcl
module "immich" {
source = "./modules/20-services-apps/immich"
appdata_path = "${module.system_globals.volume_host}/appdata/immich"
library_path = "${module.system_globals.volume_host}/data/media/photos"
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.immich.service_definition,
# Other service definitions
]
}

View File

@@ -0,0 +1,197 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the Immich container images (server and machine-learning)"
type = string
default = "release"
}
variable "appdata_path" {
description = "Base directory for Immich app data"
type = string
}
variable "library_path" {
description = "Base directory for Immich library data"
type = string
}
variable "networks" {
description = "List of networks to which the Immich server should be attached (in addition to the module network)"
type = list(string)
default = []
}
locals {
env_file = "${path.module}/.env"
monitoring = true
# Container names
server_name = "immich-server"
ml_name = "immich-machine-learning"
redis_name = "immich-redis"
postgres_name = "immich-postgres"
# Images and tags
server_image = "ghcr.io/immich-app/immich-server"
ml_image = "ghcr.io/immich-app/immich-machine-learning"
redis_image = "docker.io/valkey/valkey"
postgres_image = "ghcr.io/immich-app/postgres"
server_tag = var.image_tag
ml_tag = var.image_tag
redis_tag = "8-bookworm"
postgres_tag = "14-vectorchord0.4.3-pgvectors0.2.0"
# Ports
server_port = 2283
ml_port = 3003
# Volumes (host paths)
server_volumes = [
{
host_path = "${var.library_path}/data"
container_path = "/data"
read_only = false
}
]
ml_volumes = [
{
host_path = "${var.library_path}/ml/cache"
container_path = "/cache"
read_only = false
}
]
postgres_volumes = [
{
host_path = "${var.appdata_path}/postgres/pgdata"
container_path = "/var/lib/postgresql/data"
read_only = false
}
]
# Environment variables for Postgres
postgres_env_vars = {
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
POSTGRES_INITDB_ARGS = "--data-checksums"
}
# Environment variables for Immich server
server_env_vars = {
# Database
DB_HOSTNAME = local.postgres_name
DB_PORT = "5432"
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
DB_DATABASE_NAME = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
# Redis
REDIS_HOSTNAME = local.redis_name
REDIS_PORT = "6379"
REDIS_DBINDEX = "0"
# General
IMMICH_MEDIA_LOCATION = "/data"
}
# Healthchecks
redis_healthcheck = {
test = ["CMD", "redis-cli", "ping"]
interval = "10s"
timeout = "5s"
retries = 5
start_period = "5s"
}
postgres_healthcheck = {
test = ["CMD", "pg_isready", "-U", provider::dotenv::get_by_key("DB_USERNAME", local.env_file), "-d", provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)]
interval = "10s"
timeout = "5s"
retries = 5
start_period = "5s"
}
}
# Dedicated network for Immich
module "immich_network" {
source = "../../01-networking/docker-network"
name = "immich-network"
driver = "bridge"
}
# Valkey (Redis) service
module "redis" {
source = "../../10-services-generic/docker-service"
container_name = local.redis_name
image = local.redis_image
tag = local.redis_tag
networks = [module.immich_network.name]
monitoring = local.monitoring
healthcheck = local.redis_healthcheck
}
# Postgres service (Immich custom image)
module "postgres" {
source = "../../10-services-generic/docker-service"
container_name = local.postgres_name
image = local.postgres_image
tag = local.postgres_tag
volumes = local.postgres_volumes
env_vars = local.postgres_env_vars
networks = [module.immich_network.name]
monitoring = local.monitoring
healthcheck = local.postgres_healthcheck
}
# Immich Machine Learning service
module "machine_learning" {
source = "../../10-services-generic/docker-service"
container_name = local.ml_name
image = local.ml_image
tag = local.ml_tag
volumes = local.ml_volumes
networks = [module.immich_network.name]
monitoring = local.monitoring
}
# Immich Server service
module "immich" {
source = "../../10-services-generic/docker-service"
container_name = local.server_name
image = local.server_image
tag = local.server_tag
ports = [
{
internal = local.server_port
external = local.server_port
protocol = "tcp"
}
]
volumes = local.server_volumes
env_vars = local.server_env_vars
networks = concat([module.immich_network.name], var.networks)
monitoring = local.monitoring
depends_on = [module.postgres, module.redis]
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.server_name
primary_port = local.server_port
endpoint = "http://${local.server_name}:${local.server_port}"
subdomains = ["photos"]
publish_via = "reverse_proxy"
proxied = true
}
}

View File

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

View File

@@ -0,0 +1,87 @@
# Jellyfin Module
This module deploys Jellyfin as a Docker container and outputs a service definition to be published via your reverse proxy.
## Overview
- Container: `jellyfin` (LinuxServer.io)
- TCP 8096 for HTTP UI; UDP 7359/1900 for discovery/DLNA
- Config and media volumes mapped via variables
## Usage
```hcl
module "jellyfin" {
source = "./modules/20-services-apps/jellyfin"
volume_path = "/srv/appdata/jellyfin" # host path for Jellyfin config
data_path = "/srv/data" # host media root, mounted as /data
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | --------------------------------------------- | -------------- | ------- |
| `volume_path` | Base directory for Jellyfin config | `string` | - |
| `data_path` | Base directory for media data mounted at /data | `string` | - |
| `networks` | List of networks to attach | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | -------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "jellyfin"
primary_port = 8096
endpoint = "http://jellyfin:8096"
subdomains = ["stream"]
publish_via = "reverse_proxy"
proxied = false
}
```
## Environment Variables (.env)
This module optionally reads `HOSTNAME` from `.env` if you choose to publish a fixed external URL (see commented example in `main.tf`).
- `HOSTNAME` — your public domain (e.g., example.com). Used only if you enable `JELLYFIN_PublishedServerUrl`.
Note: `TZ`, `PUID`, and `PGID` are provided automatically by the generic docker-service module via system globals.
## Data Persistence
- `/config` -> `${volume_path}`
- `/data` -> `${data_path}`
Ensure the host paths exist and are writable by the container user.
## Dependencies
- No explicit inter-container dependencies. Healthcheck ensures readiness.
- UDP ports are exposed for discovery/DLNA.
## Integration with Networking Modules
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
## Example Integration in Main Configuration
```hcl
# In services/main.tf
module "jellyfin" {
source = "${local.module_dir}/20-services-apps/jellyfin"
volume_path = "${local.volume_host}/jellyfin"
data_path = "${local.data_host}/media"
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
}
```
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.

View File

@@ -0,0 +1,96 @@
terraform {
required_providers {
dotenv = { source = "germanbrew/dotenv" }
}
}
variable "volume_path" {
description = "Base directory for Jellyfin config"
type = string
}
variable "data_path" {
description = "Base directory for media data mounted at /data"
type = string
}
variable "networks" {
description = "List of networks to attach"
type = list(string)
default = []
}
locals {
env_file = "${path.module}/.env"
monitoring = true
container_name = "jellyfin"
image = "lscr.io/linuxserver/jellyfin"
tag = "latest"
internal_port = 8096
# UDP ports for DLNA/auto-discovery
udp_ports = [
{ internal = 7359, external = 7359, protocol = "udp" },
{ internal = 1900, external = 1900, protocol = "udp" }
]
volumes = [
{
host_path = var.volume_path,
container_path = "/config",
read_only = false
},
{
host_path = var.volume_path,
container_path = "/cache",
read_only = false
},
{
host_path = var.data_path,
container_path = "/data",
read_only = false
}
]
env_vars = {
# If you want to publish external URL, uncomment the following and set HOSTNAME in .env
JELLYFIN_PublishedServerUrl = "${provider::dotenv::get_by_key("HOSTNAME", local.env_file)}/jellyfin"
}
# Intel VAAPI/QSV: map the entire /dev/dri directory (per linuxserver/jellyfin docs)
devices = [
{
host_path = "/dev/dri/renderD128",
container_path = "/dev/dri/renderD128",
permissions = "rwm"
},
{
host_path = "/dev/dri/card0",
container_path = "/dev/dri/card0",
permissions = "rwm"
}
]
}
module "jellyfin" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.tag
volumes = local.volumes
env_vars = local.env_vars
networks = var.networks
monitoring = local.monitoring
ports = local.udp_ports
devices = local.devices
}
output "service_definition" {
description = "Service definition for Jellyfin (reverse proxy)"
value = {
name = local.container_name
primary_port = local.internal_port
endpoint = "http://${local.container_name}:${local.internal_port}"
subdomains = ["stream"]
publish_via = "reverse_proxy"
proxied = true
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +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=*

View File

@@ -1,108 +0,0 @@
# n8n Module
This module deploys [n8n](https://n8n.io/), a workflow automation tool for technical people, as Docker containers in the homelab environment.
## Overview
The n8n module:
- Deploys two Docker containers:
- `n8n`: The main workflow automation server
- `n8n-postgres`: A PostgreSQL database backend
- Creates a dedicated Docker network (`n8n-network`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## 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 and Postgres data volumes | `string` | - |
| `networks` | List of additional networks to which n8n should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for 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 = "n8n"
primary_port = 5678
endpoint = "http://n8n:5678"
subdomains = ["n8n"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Environment Variables
n8n requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
- Database configuration:
- `POSTGRES_USER`: Root PostgreSQL user
- `POSTGRES_PASSWORD`: Root PostgreSQL password
- `POSTGRES_DB`: Database name for n8n
- `POSTGRES_NON_ROOT_USER`: Non-root user for n8n to connect with
- `POSTGRES_NON_ROOT_PASSWORD`: Password for the non-root user
- n8n configuration:
- `N8N_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
## Data Persistence
n8n stores its data in two main volumes:
1. n8n application data: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
Additionally, an initialization script is mounted to the PostgreSQL container:
- `/docker-entrypoint-initdb.d/init-data.sh` in the container, from `${volume_path}/init-data.sh` on the host
## Networking
The module creates a dedicated Docker network named `n8n-network` for communication between the n8n and PostgreSQL containers. The n8n container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## 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 "n8n" {
source = "./modules/20-services-apps/n8n"
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.n8n.service_definition,
# Other service definitions
]
}
```

View File

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

View File

@@ -1,134 +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 = []
}
locals {
container_name = "n8n"
database_name = "n8n-postgres"
n8n_image = "docker.n8n.io/n8nio/n8n"
database_image = "postgres"
n8n_tag = var.image_tag != "" ? var.image_tag : "latest"
database_tag = "16"
monitoring = true
env_file = "${path.module}/.env"
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)
}
# 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"
}
}
module "n8n_network" {
source = "../../01-networking/docker-network"
name = "n8n-network"
driver = "bridge"
}
# 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
env_vars = local.database_env_vars
networks = [module.n8n_network.name]
monitoring = local.monitoring
healthcheck = local.database_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
env_vars = local.n8n_env_vars
networks = concat([module.n8n_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.n8n_internal_port
endpoint = "http://${local.container_name}:${local.n8n_internal_port}"
subdomains = ["n8n"]
publish_via = "tunnel"
}
}

View File

@@ -1,81 +0,0 @@
# NTFY Module
This module deploys [NTFY](https://ntfy.sh/), a simple HTTP-based pub-sub notification service, as a Docker container in the homelab environment.
## Overview
The NTFY module:
- Deploys the `binwiederhier/ntfy` Docker container
- Persists configuration and cache data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "ntfy" {
source = "./modules/20-services-apps/ntfy"
volume_path = "/path/to/volumes/ntfy"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the NTFY image to use | `string` | `"latest"` |
| `volume_path` | Host path for NTFY data volumes | `string` | - |
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "ntfy"
primary_port = 80
endpoint = "http://ntfy:80"
subdomains = ["ntfy"]
publish_via = "reverse_proxy" # Expose via Caddy reverse proxy
proxied = true # Proxy through Cloudflare
}
```
## Data Persistence
NTFY stores its data in two volumes:
1. Configuration: `/etc/ntfy` in the container, mapped to `${volume_path}/app` on the host
2. Cache data: `/var/cache/ntfy` in the container, mapped to `${volume_path}/cache` on the host
## Integration with Networking Modules
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`. The `proxied = true` setting ensures that the DNS record is proxied through Cloudflare.
## Example Integration in Main Configuration
```hcl
module "ntfy" {
source = "./modules/20-services-apps/ntfy"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.ntfy.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,58 +0,0 @@
variable "image_tag" {
description = "Tag of the ntfy image to use"
type = string
default = "latest"
}
variable "volume_path" {
description = "Host path for ntfy data volume"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
}
locals {
container_name = "ntfy"
image = "binwiederhier/ntfy"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
monitoring = true
exposed_port = 80
subdomains = ["ntfy"]
default_volumes = [
{
container_path = "/etc/ntfy"
host_path = "${var.volume_path}/app"
read_only = false
},
{
container_path = "/var/cache/ntfy"
host_path = "${var.volume_path}/cache"
read_only = false
}
]
}
module "ntfy" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
tag = local.image_tag
volumes = local.default_volumes
networks = var.networks
monitoring = local.monitoring
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = local.exposed_port
endpoint = "http://${local.container_name}:${local.exposed_port}"
subdomains = local.subdomains
publish_via = "reverse_proxy"
proxied = true
}
}

View File

@@ -0,0 +1,78 @@
# Portainer Module
This module deploys [Portainer](https://www.portainer.io/), a lightweight management UI that allows you to easily manage your different Docker environments.
## Overview
The Portainer module:
- Deploys one Docker container: `portainer`.
- Mounts the Docker socket to allow Portainer to manage the Docker environment.
- Persists Portainer data to a volume on the host.
- Provides a service definition for integration with networking modules.
## Usage
```hcl
module "portainer" {
source = "./modules/20-services-apps/portainer"
volume_path = "/path/to/volumes/portainer"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description |
| ------------- | ---------------------------------------------------------------- |
| `image_tag` | Tag of the Portainer image to use |
| `volume_path` | Host path for Portainer data volume |
| `networks` | List of additional networks to which Portainer should be attached |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "portainer"
primary_port = 9000
endpoint = "http://portainer:9000"
subdomains = ["portainer"]
publish_via = "reverse_proxy"
}
```
## Data Persistence
Portainer stores its data in a single volume:
1. Portainer data: `/data` in the container, mapped to `${volume_path}/data` on the host.
It also mounts the Docker socket from `/var/run/docker.sock` on the host to `/var/run/docker.sock` in the container to manage Docker.
## Example Integration in Main Configuration
```hcl
module "portainer" {
source = "./modules/20-services-apps/portainer"
volume_path = "${module.system_globals.volume_host}/portainer"
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.portainer.service_definition,
# Other service definitions
]
}
```

View File

@@ -1,13 +1,6 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the searxng container image"
description = "The tag for the portainer container image"
type = string
default = "latest"
}
@@ -24,21 +17,30 @@ variable "networks" {
}
locals {
container_name = "searxng"
image = "searxng/searxng"
tag = var.image_tag != "" ? var.image_tag : "latest"
container_name = "portainer"
image = "portainer/portainer-ce"
tag = var.image_tag
monitoring = true
internal_port = 8080
internal_port = 9000
exposed_port = 9000
# Define volumes
volumes = [
{
host_path = "${var.volume_path}/config"
container_path = "/etc/searxng"
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
read_only = false
},
{
host_path = "${var.volume_path}/data"
container_path = "/data"
read_only = false
}
]
}
module "searxng" {
# Create the portainer container
module "portainer" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.image
@@ -46,15 +48,20 @@ module "searxng" {
volumes = local.volumes
networks = var.networks
monitoring = local.monitoring
ports = [
{
internal = local.internal_port
external = local.exposed_port
protocol = "tcp"
},
]
}
output "service_definition" {
description = "Service definition with ingress configuration"
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 = ["search"]
publish_via = "tunnel"
}
}

View File

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

View File

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

View File

@@ -1,109 +0,0 @@
# Pterodactyl Panel Module
This module deploys [Pterodactyl Panel](https://pterodactyl.io/), a game server management panel, as Docker containers in the homelab environment.
## Overview
The Pterodactyl Panel module:
- Deploys three Docker containers:
- `pterodactyl-panel`: The main web UI and API server
- `pterodactyl-db`: A MariaDB database backend
- `pterodactyl-cache`: A Redis cache server
- Creates a dedicated Docker network (`ptero-panel`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "pterodactyl_panel" {
source = "./modules/20-services-apps/pterodactyl/panel"
volume_path = "/path/to/volumes/pterodactyl/panel"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the Pterodactyl Panel image to use | `string` | `"latest"` |
| `volume_path` | Host path for Pterodactyl Panel volumes | `string` | - |
| `networks` | List of networks to which the panel should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "pterodactyl-panel"
primary_port = 80
endpoint = "http://pterodactyl-panel:80"
subdomains = ["gameservers"]
publish_via = "tunnel"
}
```
## Environment Variables
Pterodactyl Panel requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider. Key variables include:
- Panel Configuration:
- `APP_URL`: The URL where the panel will be accessed
- `APP_TIMEZONE`: The timezone for the application
- `APP_SERVICE_AUTHOR`: Service author information
- Database Configuration:
- `MYSQL_PASSWORD`: Database password
- `MYSQL_ROOT_PASSWORD`: Database root password
- `MYSQL_DATABASE`: Database name
- `MYSQL_USER`: Database username
- Mail Configuration:
- Mail settings are automatically sourced from the global SMTP module
## Data Persistence
Pterodactyl Panel stores its data in multiple volumes:
1. Application data: `/app/var` in the container, mapped to `${volume_path}/var` on the host
2. Nginx configuration: `/etc/nginx/http.d` in the container, mapped to `${volume_path}/nginx` on the host
3. SSL certificates: `/etc/letsencrypt` in the container, mapped to `${volume_path}/certs` on the host
4. Logs: `/app/storage/logs` in the container, mapped to `${volume_path}/logs` on the host
5. Database data: `/var/lib/mysql` in the MariaDB container, mapped to `${volume_path}/database` on the host
## Networking
The module creates a dedicated Docker network named `ptero-panel` for communication between the panel, database, and cache containers. The panel container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## Integration with Networking Modules
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
## Example Integration in Main Configuration
```hcl
module "pterodactyl_panel" {
source = "./modules/20-services-apps/pterodactyl/panel"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.pterodactyl_panel.service_definition,
# Other service definitions
]
}
```

View File

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

View File

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

View File

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

View File

@@ -1,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
]
}
```

72
services/main.tf Normal file → Executable file
View File

@@ -1,6 +1,8 @@
locals {
module_dir = "../modules"
volume_host = module.system_globals.volume_host
root_volume = module.system_globals.volume_host
volume_host = "${module.system_globals.volume_host}/appdata"
data_host = "${module.system_globals.volume_host}/data"
}
module "system_globals" {
@@ -18,16 +20,14 @@ module "homelab_docker_network" {
subnet = "10.100.0.0/16"
}
module "actualbudget" {
source = "${local.module_dir}/20-services-apps/actualbudget"
volume_path = "${local.volume_host}/actual"
networks = [module.homelab_docker_network.name]
}
// Docker network used for media services
module "media_docker_network" {
source = "${local.module_dir}/01-networking/docker-network"
module "affine" {
source = "${local.module_dir}/20-services-apps/affine"
volume_path = "${local.volume_host}/affine"
networks = [module.homelab_docker_network.name]
name = "media-network"
driver = "bridge"
attachable = true
subnet = "10.110.0.0/16"
}
module "calibre" {
@@ -36,9 +36,25 @@ module "calibre" {
networks = [module.homelab_docker_network.name]
}
module "emulatorjs" {
source = "${local.module_dir}/20-services-apps/emulatorjs"
volume_path = "${local.volume_host}/emulatorjs"
module "glance" {
source = "${local.module_dir}/20-services-apps/glance"
volume_path = "${local.volume_host}/glance"
networks = [module.homelab_docker_network.name]
}
module "immich" {
source = "${local.module_dir}/20-services-apps/immich"
appdata_path = "${local.volume_host}/immich"
library_path = "${local.data_host}/media/photos"
networks = [module.homelab_docker_network.name]
}
module "jellyfin" {
source = "${local.module_dir}/20-services-apps/jellyfin"
volume_path = "${local.volume_host}/jellyfin"
data_path = "${local.data_host}"
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
}
module "linkwarden" {
@@ -47,32 +63,10 @@ module "linkwarden" {
networks = [module.homelab_docker_network.name]
}
module "ntfy" {
source = "${local.module_dir}/20-services-apps/ntfy"
volume_path = "${local.volume_host}/ntfy"
module "portainer" {
source = "${local.module_dir}/20-services-apps/portainer"
volume_path = "${local.volume_host}/portainer"
networks = [module.homelab_docker_network.name]
}
module "pterodactyl_panel" {
source = "${local.module_dir}/20-services-apps/pterodactyl/panel"
volume_path = "${local.volume_host}/pterodactyl/panel"
networks = [module.homelab_docker_network.name]
}
module "pterodactyl_wings" {
source = "${local.module_dir}/20-services-apps/pterodactyl/wings"
volume_path = "${local.volume_host}/pterodactyl/wings"
networks = [module.homelab_docker_network.name]
}
module "n8n" {
source = "${local.module_dir}/20-services-apps/n8n"
volume_path = "${local.volume_host}/n8n"
networks = [module.homelab_docker_network.name]
}
module "searxng" {
source = "${local.module_dir}/20-services-apps/searxng"
volume_path = "${local.volume_host}/searxng"
networks = [module.homelab_docker_network.name]
}

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

@@ -4,16 +4,12 @@
output "service_definitions" {
description = "Service definitions for all services"
value = [
module.actualbudget.service_definition,
module.affine.service_definition,
module.calibre.service_definition,
module.emulatorjs.service_definition,
module.glance.service_definition,
module.immich.service_definition,
module.jellyfin.service_definition,
module.linkwarden.service_definition,
module.ntfy.service_definition,
module.pterodactyl_wings.service_definition,
module.pterodactyl_panel.service_definition,
module.n8n.service_definition,
module.searxng.service_definition
module.portainer.service_definition
]
}