Compare commits
1 Commits
main
...
media-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07f59d021 |
@@ -60,7 +60,7 @@ homelab/
|
||||
│ └── docker-service/ # Generic module for deploying Docker containers
|
||||
└── 20-services-apps/ # Application-specific wrapper modules
|
||||
├── jellyfin/
|
||||
├── calibre/
|
||||
├── affine/
|
||||
└── ... # Other application modules
|
||||
│
|
||||
└── services/ # Application services (Docker containers)
|
||||
|
||||
6
main.tf
6
main.tf
@@ -19,10 +19,6 @@ module "services" {
|
||||
source = "./services"
|
||||
}
|
||||
|
||||
locals {
|
||||
volume_host = "${module.system_globals.volume_host}/appdata"
|
||||
}
|
||||
|
||||
module "homelab_cloudflared_tunnel" {
|
||||
source = "./modules/01-networking/cloudflared-tunnel"
|
||||
cloudflare_account_id = module.cloudflare_globals.cloudflare_account_id
|
||||
@@ -44,7 +40,7 @@ module "homelab_caddy_proxy" {
|
||||
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
|
||||
external_ip = module.cloudflare_globals.external_ip
|
||||
service_definitions = module.services.service_definitions
|
||||
volume_path = local.volume_host
|
||||
volume_path = module.system_globals.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
monitoring = true
|
||||
}
|
||||
|
||||
@@ -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 jellyfin (reverse-proxy only with direct IP exposure)
|
||||
# Example based on ntfy (reverse-proxy only with direct IP exposure)
|
||||
output "service_definition" {
|
||||
description = "Service definition for a media server"
|
||||
description = "Service definition for a notification service"
|
||||
value = {
|
||||
name = "jellyfin"
|
||||
primary_port = 8096
|
||||
endpoint = "http://jellyfin:8096"
|
||||
subdomains = ["media"]
|
||||
name = "ntfy"
|
||||
primary_port = 80
|
||||
endpoint = "http://ntfy:80"
|
||||
subdomains = ["ntfy"]
|
||||
publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy
|
||||
proxied = false # Don't proxy through Cloudflare (expose direct IP)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ locals {
|
||||
])
|
||||
}
|
||||
|
||||
resource "docker_volume" "caddy_config" {
|
||||
name = "${local.container_name}_config"
|
||||
}
|
||||
|
||||
// Create Caddyfile in the volume path
|
||||
resource "local_file" "caddyfile" {
|
||||
content = local.caddyfile_content
|
||||
@@ -111,12 +115,12 @@ module "caddy" {
|
||||
|
||||
ports = [
|
||||
{
|
||||
external = "80"
|
||||
external = "9080"
|
||||
internal = "80"
|
||||
protocol = "tcp"
|
||||
},
|
||||
{
|
||||
external = "443"
|
||||
external = "9443"
|
||||
internal = "443"
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ module "homelab_tunnel" {
|
||||
tunnel_name = "homelab-tunnel"
|
||||
ingress_rules = [
|
||||
{
|
||||
hostname = "media.${module.cloudflare_globals.domain}"
|
||||
service = "http://jellyfin:8096"
|
||||
hostname = "budget.${module.cloudflare_globals.domain}"
|
||||
service = "http://actualbudget:5006"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 :
|
||||
|
||||
@@ -139,36 +139,15 @@ resource "docker_container" "service_container" {
|
||||
cpu_shares = var.cpu_shares
|
||||
|
||||
# Other container options
|
||||
dns = var.dns
|
||||
dns_search = var.dns_search
|
||||
hostname = var.hostname
|
||||
domainname = var.domainname
|
||||
user = var.user
|
||||
group_add = var.group_add
|
||||
working_dir = var.working_dir
|
||||
command = var.command
|
||||
entrypoint = var.entrypoint
|
||||
privileged = var.privileged
|
||||
destroy_grace_seconds = var.destroy_grace_seconds
|
||||
|
||||
# Linux capabilities controls
|
||||
dynamic "capabilities" {
|
||||
for_each = length(var.capabilities_add) > 0 || length(var.capabilities_drop) > 0 ? [1] : []
|
||||
content {
|
||||
add = var.capabilities_add
|
||||
drop = var.capabilities_drop
|
||||
}
|
||||
}
|
||||
|
||||
# Device mappings
|
||||
dynamic "devices" {
|
||||
for_each = var.devices
|
||||
content {
|
||||
host_path = devices.value.host_path
|
||||
container_path = devices.value.container_path
|
||||
permissions = devices.value.permissions
|
||||
}
|
||||
}
|
||||
dns = var.dns
|
||||
dns_search = var.dns_search
|
||||
hostname = var.hostname
|
||||
domainname = var.domainname
|
||||
user = var.user
|
||||
working_dir = var.working_dir
|
||||
command = var.command
|
||||
entrypoint = var.entrypoint
|
||||
privileged = var.privileged
|
||||
|
||||
# Set log options
|
||||
log_driver = var.log_driver
|
||||
|
||||
@@ -179,48 +179,12 @@ variable "entrypoint" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group_add" {
|
||||
description = "Additional groups to add to the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "privileged" {
|
||||
description = "Run container in privileged mode"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
// Linux capabilities controls
|
||||
variable "capabilities_add" {
|
||||
description = "Linux capabilities to add to the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "capabilities_drop" {
|
||||
description = "Linux capabilities to drop from the container"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
// Devices to pass through to container
|
||||
variable "devices" {
|
||||
description = "List of device mappings for the container"
|
||||
type = list(object({
|
||||
host_path = string
|
||||
container_path = string
|
||||
permissions = string
|
||||
}))
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "destroy_grace_seconds" {
|
||||
description = "Grace period in seconds before the container is destroyed"
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
|
||||
// Logging options
|
||||
variable "log_driver" {
|
||||
description = "Log driver for the container"
|
||||
@@ -231,8 +195,8 @@ variable "log_driver" {
|
||||
variable "log_opts" {
|
||||
description = "Log driver options"
|
||||
type = map(string)
|
||||
default = {
|
||||
max-size = "10m"
|
||||
max-file = "3"
|
||||
default = {
|
||||
max-size = "10m"
|
||||
max-file = "3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Glance Module
|
||||
# ActualBudget Module
|
||||
|
||||
This module deploys [Glance](https://glanceapp.io/), a dashboard application, as a Docker container in the homelab environment.
|
||||
This module deploys [ActualBudget](https://actualbudget.com/), a personal finance and budgeting application, as a Docker container in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Glance module:
|
||||
The ActualBudget module:
|
||||
|
||||
- Deploys the `glanceapp/glance` Docker container
|
||||
- Persists configuration to a volume on the host
|
||||
- Deploys the `actualbudget/actual-server` Docker container
|
||||
- Persists data to a volume on the host
|
||||
- Provides service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "glance" {
|
||||
source = "./modules/20-services-apps/glance"
|
||||
volume_path = "/path/to/volumes/glance"
|
||||
module "actualbudget" {
|
||||
source = "./modules/20-services-apps/actualbudget"
|
||||
volume_path = "/path/to/volumes/actualbudget"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
@@ -24,8 +24,8 @@ module "glance" {
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the Glance image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for Glance data volume | `string` | - |
|
||||
| `image_tag` | Tag of the ActualBudget image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for ActualBudget data volume | `string` | - |
|
||||
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
|
||||
|
||||
## Outputs
|
||||
@@ -40,17 +40,17 @@ This module outputs a service definition that is used by the networking modules
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "glance"
|
||||
primary_port = 4921
|
||||
endpoint = "http://glance:4921"
|
||||
subdomains = ["glance"]
|
||||
name = "actualbudget"
|
||||
primary_port = 5006
|
||||
endpoint = "http://actualbudget:5006"
|
||||
subdomains = ["budget"]
|
||||
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Glance stores its configuration in the `/app/config` directory inside the container. This is mapped to a volume on the host at `${volume_path}/config`.
|
||||
ActualBudget stores its data in the `/data` directory inside the container. This is mapped to a volume on the host at `${volume_path}/data`.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
@@ -59,8 +59,8 @@ This service is configured to be exposed through a Cloudflare tunnel for secure
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "glance" {
|
||||
source = "./modules/20-services-apps/glance"
|
||||
module "actualbudget" {
|
||||
source = "./modules/20-services-apps/actualbudget"
|
||||
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.glance.service_definition,
|
||||
module.actualbudget.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
variable "image_tag" {
|
||||
description = "Tag of the Glance image to use"
|
||||
description = "Tag of the ActualBudget image to use"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Host path for Glance data volume"
|
||||
description = "Host path for ActualBudget data volume"
|
||||
type = string
|
||||
}
|
||||
|
||||
@@ -15,22 +15,22 @@ variable "networks" {
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "glance"
|
||||
image = "glanceapp/glance"
|
||||
container_name = "actualbudget"
|
||||
image = "actualbudget/actual-server"
|
||||
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
monitoring = true
|
||||
host_port = 8080
|
||||
subdomains = ["glance"]
|
||||
exposed_port = 5006
|
||||
subdomains = ["budget"]
|
||||
default_volumes = [
|
||||
{
|
||||
container_path = "/app/config"
|
||||
host_path = "${var.volume_path}/config"
|
||||
container_path = "/data"
|
||||
host_path = "${var.volume_path}/data"
|
||||
read_only = false
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "glance" {
|
||||
module "actualbudget" {
|
||||
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.host_port
|
||||
endpoint = "http://${local.container_name}:${local.host_port}"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "tunnel"
|
||||
primary_port = local.exposed_port
|
||||
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
17
modules/20-services-apps/affine/.env.example
Normal file
17
modules/20-services-apps/affine/.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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=
|
||||
121
modules/20-services-apps/affine/README.md
Normal file
121
modules/20-services-apps/affine/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
198
modules/20-services-apps/affine/main.tf
Normal file
198
modules/20-services-apps/affine/main.tf
Normal file
@@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
102
modules/20-services-apps/calibre/README.md
Normal file
102
modules/20-services-apps/calibre/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
104
modules/20-services-apps/calibre/main.tf
Normal file
104
modules/20-services-apps/calibre/main.tf
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
3
modules/20-services-apps/emulatorjs/.env.example
Normal file
3
modules/20-services-apps/emulatorjs/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
EMULATORJS_FRONTEND_PORT=5823
|
||||
EMULATORJS_CONFIG_PORT=5824
|
||||
EMULATORJS_BACKEND_PORT=5825
|
||||
99
modules/20-services-apps/emulatorjs/README.md
Normal file
99
modules/20-services-apps/emulatorjs/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 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
|
||||
78
modules/20-services-apps/emulatorjs/main.tf
Normal file
78
modules/20-services-apps/emulatorjs/main.tf
Normal file
@@ -0,0 +1,78 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "The tag for the EmulatorJS container image"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "emulatorjs"
|
||||
image = "linuxserver/emulatorjs"
|
||||
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
monitoring = true
|
||||
env_file = "${path.module}/.env"
|
||||
frontend_port = provider::dotenv::get_by_key("EMULATORJS_FRONTEND_PORT", local.env_file)
|
||||
config_port = provider::dotenv::get_by_key("EMULATORJS_CONFIG_PORT", local.env_file)
|
||||
backend_port = provider::dotenv::get_by_key("EMULATORJS_BACKEND_PORT", local.env_file)
|
||||
ports = [
|
||||
{
|
||||
internal = 3000
|
||||
external = local.config_port
|
||||
protocol = "tcp"
|
||||
},
|
||||
{
|
||||
internal = 80
|
||||
external = local.frontend_port
|
||||
protocol = "tcp"
|
||||
},
|
||||
{
|
||||
internal = 4001
|
||||
external = local.backend_port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/config"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = "${var.volume_path}/data"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "emulatorjs" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.image_tag
|
||||
volumes = local.volumes
|
||||
ports = local.ports
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = module.emulatorjs.container_name
|
||||
primary_port = local.frontend_port
|
||||
endpoint = "http://${module.emulatorjs.container_name}:${local.frontend_port}"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
|
||||
|
||||
###################################################################################
|
||||
# Required database configuration (used by Terraform to configure Postgres & Immich)
|
||||
###################################################################################
|
||||
|
||||
# PostgreSQL username
|
||||
DB_USERNAME=postgres
|
||||
|
||||
# PostgreSQL password (use only A-Za-z0-9 characters)
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# PostgreSQL database name
|
||||
DB_DATABASE_NAME=immich
|
||||
@@ -1,110 +0,0 @@
|
||||
# Immich Module
|
||||
|
||||
This module deploys [Immich](https://immich.app/), a high-performance self-hosted photo and video backup solution, as Docker containers in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Immich module:
|
||||
|
||||
- Deploys four Docker containers:
|
||||
- `immich-server`: The main Immich API/UI server (port 2283)
|
||||
- `immich-machine-learning`: The ML service for search, faces, and embeddings
|
||||
- `immich-postgres`: Immich-tuned PostgreSQL database
|
||||
- `immich-redis`: Valkey/Redis-compatible cache
|
||||
- Creates a dedicated Docker network (`immich-network`) for inter-container communication
|
||||
- Persists data to volumes on the host
|
||||
- Provides a service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "immich" {
|
||||
source = "./modules/20-services-apps/immich"
|
||||
appdata_path = "/path/to/appdata/immich"
|
||||
library_path = "/path/to/data/media/photos"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| --------------- | -------------------------------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the Immich images to use (`server` and `machine-learning`) | `string` | `"release"` |
|
||||
| `appdata_path` | Base host path for Immich app data (e.g., PostgreSQL data and internal configs) | `string` | - |
|
||||
| `library_path` | Base host path for user library data and ML cache | `string` | - |
|
||||
| `networks` | List of additional networks to which the server should attach | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition used by networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "immich-server"
|
||||
primary_port = 2283
|
||||
endpoint = "http://immich-server:2283"
|
||||
subdomains = ["photos"]
|
||||
publish_via = "reverse_proxy"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Only the database credentials are expected in a `.env` file in this module directory and are read using the `dotenv` Terraform provider. Everything else is configured directly in Terraform.
|
||||
|
||||
Required in `modules/20-services-apps/immich/.env`:
|
||||
|
||||
- `DB_USERNAME`: PostgreSQL user
|
||||
- `DB_PASSWORD`: PostgreSQL password
|
||||
- `DB_DATABASE_NAME`: Database name
|
||||
|
||||
A ready-to-copy `modules/20-services-apps/immich/.env.example` is provided.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Immich stores data in the following volumes:
|
||||
|
||||
1. Library storage: `/data` in `immich-server`, mapped to `${library_path}/library` on the host
|
||||
2. ML model cache: `/cache` in `immich-machine-learning`, mapped to `${library_path}/machine-learning/cache` on the host
|
||||
3. PostgreSQL data: `/var/lib/postgresql/data` in `immich-postgres`, mapped to `${appdata_path}/postgres/pgdata` on the host
|
||||
|
||||
## Networking
|
||||
|
||||
The module creates a dedicated Docker network named `immich-network` for communication between Immich components. The Immich server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `immich-server` depends on `immich-postgres` and `immich-redis`
|
||||
- `immich-postgres` and `immich-redis` include healthchecks
|
||||
- The ML service is independent and discovered by the server internally; tuning can be done via the Immich admin UI
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "immich" {
|
||||
source = "./modules/20-services-apps/immich"
|
||||
appdata_path = "${module.system_globals.volume_host}/appdata/immich"
|
||||
library_path = "${module.system_globals.volume_host}/data/media/photos"
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.immich.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "The tag for the Immich container images (server and machine-learning)"
|
||||
type = string
|
||||
default = "release"
|
||||
}
|
||||
|
||||
variable "appdata_path" {
|
||||
description = "Base directory for Immich app data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "library_path" {
|
||||
description = "Base directory for Immich library data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the Immich server should be attached (in addition to the module network)"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
monitoring = true
|
||||
|
||||
# Container names
|
||||
server_name = "immich-server"
|
||||
ml_name = "immich-machine-learning"
|
||||
redis_name = "immich-redis"
|
||||
postgres_name = "immich-postgres"
|
||||
|
||||
# Images and tags
|
||||
server_image = "ghcr.io/immich-app/immich-server"
|
||||
ml_image = "ghcr.io/immich-app/immich-machine-learning"
|
||||
redis_image = "docker.io/valkey/valkey"
|
||||
postgres_image = "ghcr.io/immich-app/postgres"
|
||||
|
||||
server_tag = var.image_tag
|
||||
ml_tag = var.image_tag
|
||||
redis_tag = "8-bookworm"
|
||||
postgres_tag = "14-vectorchord0.4.3-pgvectors0.2.0"
|
||||
|
||||
# Ports
|
||||
server_port = 2283
|
||||
ml_port = 3003
|
||||
|
||||
# Volumes (host paths)
|
||||
server_volumes = [
|
||||
{
|
||||
host_path = "${var.library_path}/data"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
ml_volumes = [
|
||||
{
|
||||
host_path = "${var.library_path}/ml/cache"
|
||||
container_path = "/cache"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
postgres_volumes = [
|
||||
{
|
||||
host_path = "${var.appdata_path}/postgres/pgdata"
|
||||
container_path = "/var/lib/postgresql/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
# Environment variables for Postgres
|
||||
postgres_env_vars = {
|
||||
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
|
||||
POSTGRES_INITDB_ARGS = "--data-checksums"
|
||||
}
|
||||
|
||||
# Environment variables for Immich server
|
||||
server_env_vars = {
|
||||
# Database
|
||||
DB_HOSTNAME = local.postgres_name
|
||||
DB_PORT = "5432"
|
||||
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||
DB_DATABASE_NAME = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)
|
||||
|
||||
# Redis
|
||||
REDIS_HOSTNAME = local.redis_name
|
||||
REDIS_PORT = "6379"
|
||||
REDIS_DBINDEX = "0"
|
||||
|
||||
# General
|
||||
IMMICH_MEDIA_LOCATION = "/data"
|
||||
}
|
||||
|
||||
# Healthchecks
|
||||
redis_healthcheck = {
|
||||
test = ["CMD", "redis-cli", "ping"]
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
retries = 5
|
||||
start_period = "5s"
|
||||
}
|
||||
|
||||
postgres_healthcheck = {
|
||||
test = ["CMD", "pg_isready", "-U", provider::dotenv::get_by_key("DB_USERNAME", local.env_file), "-d", provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)]
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
retries = 5
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
# Dedicated network for Immich
|
||||
module "immich_network" {
|
||||
source = "../../01-networking/docker-network"
|
||||
name = "immich-network"
|
||||
driver = "bridge"
|
||||
}
|
||||
|
||||
# Valkey (Redis) service
|
||||
module "redis" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.redis_name
|
||||
image = local.redis_image
|
||||
tag = local.redis_tag
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.redis_healthcheck
|
||||
}
|
||||
|
||||
# Postgres service (Immich custom image)
|
||||
module "postgres" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.postgres_name
|
||||
image = local.postgres_image
|
||||
tag = local.postgres_tag
|
||||
volumes = local.postgres_volumes
|
||||
env_vars = local.postgres_env_vars
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.postgres_healthcheck
|
||||
}
|
||||
|
||||
# Immich Machine Learning service
|
||||
module "machine_learning" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.ml_name
|
||||
image = local.ml_image
|
||||
tag = local.ml_tag
|
||||
volumes = local.ml_volumes
|
||||
networks = [module.immich_network.name]
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
# Immich Server service
|
||||
module "immich" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.server_name
|
||||
image = local.server_image
|
||||
tag = local.server_tag
|
||||
ports = [
|
||||
{
|
||||
internal = local.server_port
|
||||
external = local.server_port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
volumes = local.server_volumes
|
||||
env_vars = local.server_env_vars
|
||||
networks = concat([module.immich_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
depends_on = [module.postgres, module.redis]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.server_name
|
||||
primary_port = local.server_port
|
||||
endpoint = "http://${local.server_name}:${local.server_port}"
|
||||
subdomains = ["photos"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Optional values for Jellyfin module
|
||||
# Only needed if you enable JELLYFIN_PublishedServerUrl in main.tf
|
||||
|
||||
HOSTNAME=example.com
|
||||
@@ -1,87 +0,0 @@
|
||||
# Jellyfin Module
|
||||
|
||||
This module deploys Jellyfin as a Docker container and outputs a service definition to be published via your reverse proxy.
|
||||
|
||||
## Overview
|
||||
|
||||
- Container: `jellyfin` (LinuxServer.io)
|
||||
- TCP 8096 for HTTP UI; UDP 7359/1900 for discovery/DLNA
|
||||
- Config and media volumes mapped via variables
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "jellyfin" {
|
||||
source = "./modules/20-services-apps/jellyfin"
|
||||
volume_path = "/srv/appdata/jellyfin" # host path for Jellyfin config
|
||||
data_path = "/srv/data" # host media root, mounted as /data
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | --------------------------------------------- | -------------- | ------- |
|
||||
| `volume_path` | Base directory for Jellyfin config | `string` | - |
|
||||
| `data_path` | Base directory for media data mounted at /data | `string` | - |
|
||||
| `networks` | List of networks to attach | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "jellyfin"
|
||||
primary_port = 8096
|
||||
endpoint = "http://jellyfin:8096"
|
||||
subdomains = ["stream"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables (.env)
|
||||
|
||||
This module optionally reads `HOSTNAME` from `.env` if you choose to publish a fixed external URL (see commented example in `main.tf`).
|
||||
|
||||
- `HOSTNAME` — your public domain (e.g., example.com). Used only if you enable `JELLYFIN_PublishedServerUrl`.
|
||||
|
||||
Note: `TZ`, `PUID`, and `PGID` are provided automatically by the generic docker-service module via system globals.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
- `/config` -> `${volume_path}`
|
||||
- `/data` -> `${data_path}`
|
||||
|
||||
Ensure the host paths exist and are writable by the container user.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No explicit inter-container dependencies. Healthcheck ensures readiness.
|
||||
- UDP ports are exposed for discovery/DLNA.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
# In services/main.tf
|
||||
module "jellyfin" {
|
||||
source = "${local.module_dir}/20-services-apps/jellyfin"
|
||||
volume_path = "${local.volume_host}/jellyfin"
|
||||
data_path = "${local.data_host}/media"
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.
|
||||
@@ -1,96 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = { source = "germanbrew/dotenv" }
|
||||
}
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for Jellyfin config"
|
||||
type = string
|
||||
}
|
||||
variable "data_path" {
|
||||
description = "Base directory for media data mounted at /data"
|
||||
type = string
|
||||
}
|
||||
variable "networks" {
|
||||
description = "List of networks to attach"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
monitoring = true
|
||||
container_name = "jellyfin"
|
||||
image = "lscr.io/linuxserver/jellyfin"
|
||||
tag = "latest"
|
||||
internal_port = 8096
|
||||
|
||||
# UDP ports for DLNA/auto-discovery
|
||||
udp_ports = [
|
||||
{ internal = 7359, external = 7359, protocol = "udp" },
|
||||
{ internal = 1900, external = 1900, protocol = "udp" }
|
||||
]
|
||||
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/config",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/cache",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.data_path,
|
||||
container_path = "/data",
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
env_vars = {
|
||||
# If you want to publish external URL, uncomment the following and set HOSTNAME in .env
|
||||
JELLYFIN_PublishedServerUrl = "${provider::dotenv::get_by_key("HOSTNAME", local.env_file)}/jellyfin"
|
||||
}
|
||||
|
||||
# Intel VAAPI/QSV: map the entire /dev/dri directory (per linuxserver/jellyfin docs)
|
||||
devices = [
|
||||
{
|
||||
host_path = "/dev/dri/renderD128",
|
||||
container_path = "/dev/dri/renderD128",
|
||||
permissions = "rwm"
|
||||
},
|
||||
{
|
||||
host_path = "/dev/dri/card0",
|
||||
container_path = "/dev/dri/card0",
|
||||
permissions = "rwm"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "jellyfin" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.volumes
|
||||
env_vars = local.env_vars
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = local.udp_ports
|
||||
devices = local.devices
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for Jellyfin (reverse proxy)"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
subdomains = ["stream"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
180
modules/20-services-apps/media-server/README.md
Normal file
180
modules/20-services-apps/media-server/README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Media Server Module
|
||||
|
||||
This module deploys a complete media server stack using Docker containers in the homelab environment. It includes content management services (Sonarr, Radarr, Readarr), content discovery (Prowlarr, Jellyseerr), download clients (Qbittorrent, Sabnzbd), and media playback (Jellyfin).
|
||||
|
||||
## Overview
|
||||
|
||||
The Media Server module:
|
||||
|
||||
- Deploys multiple Docker containers:
|
||||
- `sonarr`: Series/TV management system
|
||||
- `radarr`: Movie management system
|
||||
- `readarr`: Book management system
|
||||
- `jellyseerr`: Content request and discovery system
|
||||
- `prowlarr`: Indexer management system
|
||||
- `qbittorrent`: Torrent download client with VueTorrent UI
|
||||
- `unpackerr`: Automatic extraction utility
|
||||
- `jellyfin`: Media server for streaming content
|
||||
- `sabnzbd`: Usenet download client
|
||||
- `flaresolverr`: Proxy service to bypass Cloudflare and other protection
|
||||
- `autoheal`: Service for automatic container health restarts
|
||||
- Creates a dedicated Docker network (`media-server`) for container communication
|
||||
- Persists data to volumes on the host
|
||||
- Provides service definitions for integration with networking modules
|
||||
- Exposes Jellyfin, Jellyseerr, and Sabnzbd via reverse proxy
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "media_server" {
|
||||
source = "./modules/20-services-apps/media-server"
|
||||
volume_path = "/path/to/app/data"
|
||||
data_root = "/path/to/media/data"
|
||||
download_root = "/path/to/download/data"
|
||||
user_id = "1000"
|
||||
group_id = "1000"
|
||||
timezone = "UTC"
|
||||
hostname = "example.com"
|
||||
sonarr_api_key = "your-sonarr-api-key"
|
||||
radarr_api_key = "your-radarr-api-key"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ---------------- | ------------------------------------------------- | -------------- | -------- |
|
||||
| `volume_path` | Base directory for application data | `string` | - |
|
||||
| `data_root` | Root directory for media data | `string` | - |
|
||||
| `download_root` | Directory for downloads | `string` | - |
|
||||
| `user_id` | User ID for container permissions | `string` | `"1000"` |
|
||||
| `group_id` | Group ID for container permissions | `string` | `"1000"` |
|
||||
| `timezone` | Timezone for the containers | `string` | `"UTC"` |
|
||||
| `hostname` | Hostname for the Jellyfin PublishedServerUrl | `string` | - |
|
||||
| `sonarr_api_key` | API key for Sonarr | `string` | - |
|
||||
| `radarr_api_key` | API key for Radarr | `string` | - |
|
||||
| `networks` | List of networks to which containers are attached | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| --------------------- | ------------------------------------------------------------------ |
|
||||
| `service_definitions` | Service definitions for Jellyfin, Jellyseerr, and Sabnzbd services |
|
||||
| `network_name` | Name of the media server network |
|
||||
|
||||
## Services
|
||||
|
||||
### Sonarr
|
||||
TV series management tool that integrates with various download clients and indexers to automate obtaining TV episodes.
|
||||
- Port: 8989
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/sonarr`
|
||||
- Data: `/data` → `${data_root}`
|
||||
|
||||
### Radarr
|
||||
Movie management tool that integrates with various download clients and indexers to automate obtaining movies.
|
||||
- Port: 7878
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/radarr`
|
||||
- Data: `/data` → `${data_root}`
|
||||
|
||||
### Readarr
|
||||
Book management tool that integrates with various download clients and indexers to automate obtaining books.
|
||||
- Port: 8787
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/readarr`
|
||||
- Books: `/books` → `${data_root}`
|
||||
|
||||
### Jellyseerr
|
||||
Media request system that integrates with Jellyfin to allow users to request new content.
|
||||
- Port: 5055
|
||||
- Volumes:
|
||||
- Config: `/app/config` → `${volume_path}/jellyseerr`
|
||||
|
||||
### Prowlarr
|
||||
Indexer manager/proxy that integrates with various PVR apps and download clients.
|
||||
- Port: 9696
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/prowlarr`
|
||||
|
||||
### FlareSolverr
|
||||
Proxy server to bypass Cloudflare and other protection methods.
|
||||
- Port: 8191
|
||||
|
||||
### QBittorrent
|
||||
Torrent client with VueTorrent web interface.
|
||||
- Port: 8080
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/qbittorrent`
|
||||
- Downloads: `/data/torrents` → `${download_root}`
|
||||
|
||||
### Unpackerr
|
||||
Extracts completed downloads automatically for Sonarr, Radarr, and others.
|
||||
- Volumes:
|
||||
- Downloads: `/data/torrents` → `${download_root}`
|
||||
|
||||
### Jellyfin
|
||||
Media server for streaming content to various devices.
|
||||
- Ports:
|
||||
- 8096 (HTTP)
|
||||
- 7359 (UDP)
|
||||
- 1900 (UDP)
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/jellyfin`
|
||||
- Data: `/data` → `${data_root}`
|
||||
- Devices:
|
||||
- `/dev/dri/` for hardware acceleration
|
||||
|
||||
### Sabnzbd
|
||||
Usenet download client.
|
||||
- Port: 6789 (external) → 8080 (internal)
|
||||
- Volumes:
|
||||
- Config: `/config` → `${volume_path}/sabnzbd/config`
|
||||
- Downloads: `/downloads` → `${data_root}/usenet/downloads`
|
||||
|
||||
### Autoheal
|
||||
Service that automatically checks for container health and restarts unhealthy containers.
|
||||
- Requires access to Docker socket
|
||||
|
||||
## Networking
|
||||
|
||||
The module creates a dedicated Docker network named `media-server` for communication between the components. Each service container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||
|
||||
## Reverse Proxy Integration
|
||||
|
||||
Three services are configured to be exposed through a reverse proxy:
|
||||
|
||||
- **Jellyfin**: Exposed at subdomain `jellyfin`
|
||||
- **Jellyseerr**: Exposed at subdomain `requests`
|
||||
- **Sabnzbd**: Exposed at subdomain `sabnzbd`
|
||||
|
||||
These services have their `publish_via` property set to `"reverse_proxy"` in their service definitions.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "media_server" {
|
||||
source = "./modules/20-services-apps/media-server"
|
||||
volume_path = module.system_globals.volume_host
|
||||
data_root = "${module.system_globals.data_root}/media"
|
||||
download_root = "${module.system_globals.data_root}/downloads"
|
||||
user_id = module.system_globals.user_id
|
||||
group_id = module.system_globals.group_id
|
||||
timezone = module.system_globals.timezone
|
||||
hostname = "example.com"
|
||||
sonarr_api_key = var.sonarr_api_key
|
||||
radarr_api_key = var.radarr_api_key
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definitions are automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = concat(
|
||||
module.media_server.service_definitions,
|
||||
# Other service definitions
|
||||
)
|
||||
}
|
||||
```
|
||||
148
modules/20-services-apps/media-server/main.tf
Normal file
148
modules/20-services-apps/media-server/main.tf
Normal file
@@ -0,0 +1,148 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
monitoring = true
|
||||
|
||||
# Define common healthcheck settings
|
||||
healthcheck_interval = "30s"
|
||||
healthcheck_retries = 10
|
||||
}
|
||||
|
||||
# Create dedicated network for media server components
|
||||
module "media_server_network" {
|
||||
source = "../../01-networking/docker-network"
|
||||
name = "media-server"
|
||||
subnet = "11.102.0.0/16"
|
||||
driver = "bridge"
|
||||
}
|
||||
|
||||
# Import service modules
|
||||
module "sonarr" {
|
||||
source = "./services/sonarr"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
data_root = var.data_root
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "radarr" {
|
||||
source = "./services/radarr"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
data_root = var.data_root
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "readarr" {
|
||||
source = "./services/readarr"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
data_root = var.data_root
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "jellyseerr" {
|
||||
source = "./services/jellyseerr"
|
||||
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "prowlarr" {
|
||||
source = "./services/prowlarr"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "qbittorrent" {
|
||||
source = "./services/qbittorrent"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
download_root = var.download_root
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "unpackerr" {
|
||||
source = "./services/unpackerr"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
download_root = var.download_root
|
||||
sonarr_api_key = var.sonarr_api_key
|
||||
radarr_api_key = var.radarr_api_key
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "jellyfin" {
|
||||
source = "./services/jellyfin"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
data_root = var.data_root
|
||||
hostname = var.hostname
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "sabnzbd" {
|
||||
source = "./services/sabnzbd"
|
||||
|
||||
user_id = var.user_id
|
||||
group_id = var.group_id
|
||||
timezone = var.timezone
|
||||
volume_path = var.volume_path
|
||||
data_root = var.data_root
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "flaresolverr" {
|
||||
source = "./services/flaresolverr"
|
||||
|
||||
timezone = var.timezone
|
||||
log_level = "info"
|
||||
log_html = "false"
|
||||
captcha_solver = "none"
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "autoheal" {
|
||||
source = "./services/autoheal"
|
||||
|
||||
networks = concat([module.media_server_network.name], var.networks)
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
13
modules/20-services-apps/media-server/outputs.tf
Normal file
13
modules/20-services-apps/media-server/outputs.tf
Normal file
@@ -0,0 +1,13 @@
|
||||
output "service_definitions" {
|
||||
description = "Service definitions for integration with networking modules"
|
||||
value = [
|
||||
module.jellyfin.service_definition,
|
||||
module.jellyseerr.service_definition,
|
||||
module.sabnzbd.service_definition
|
||||
]
|
||||
}
|
||||
|
||||
output "network_name" {
|
||||
description = "Name of the media server network"
|
||||
value = module.media_server_network.name
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "autoheal"
|
||||
image = "willfarrell/autoheal"
|
||||
tag = "latest"
|
||||
|
||||
autoheal_env_vars = {
|
||||
AUTOHEAL_CONTAINER_LABEL = "all"
|
||||
}
|
||||
|
||||
autoheal_volumes = [
|
||||
{
|
||||
host_path = "/var/run/docker.sock"
|
||||
container_path = "/var/run/docker.sock"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "autoheal" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.autoheal_volumes
|
||||
env_vars = local.autoheal_env_vars
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for autoheal"
|
||||
value = {
|
||||
name = local.container_name
|
||||
endpoint = "http://${local.container_name}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "log_level" {
|
||||
description = "Log level for flaresolverr"
|
||||
type = string
|
||||
default = "info"
|
||||
}
|
||||
|
||||
variable "log_html" {
|
||||
description = "Whether to log HTML"
|
||||
type = string
|
||||
default = "false"
|
||||
}
|
||||
|
||||
variable "captcha_solver" {
|
||||
description = "Type of CAPTCHA solver to use"
|
||||
type = string
|
||||
default = "none"
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "flaresolverr"
|
||||
image = "ghcr.io/flaresolverr/flaresolverr"
|
||||
tag = "latest"
|
||||
|
||||
flaresolverr_env_vars = {
|
||||
LOG_LEVEL = var.log_level
|
||||
LOG_HTML = var.log_html
|
||||
CAPTCHA_SOLVER = var.captcha_solver
|
||||
TZ = var.timezone
|
||||
}
|
||||
}
|
||||
|
||||
module "flaresolverr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
env_vars = local.flaresolverr_env_vars
|
||||
ports = [{
|
||||
internal = 8191
|
||||
external = 8191
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for flaresolverr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8191
|
||||
endpoint = "http://${local.container_name}:8191"
|
||||
}
|
||||
}
|
||||
114
modules/20-services-apps/media-server/services/jellyfin/main.tf
Normal file
114
modules/20-services-apps/media-server/services/jellyfin/main.tf
Normal file
@@ -0,0 +1,114 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "hostname" {
|
||||
description = "Hostname for the Jellyfin PublishedServerUrl"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "jellyfin"
|
||||
image = "jellyfin/jellyfin"
|
||||
tag = "latest"
|
||||
|
||||
internal_ports = [
|
||||
{
|
||||
internal = 8096
|
||||
external = 8096
|
||||
protocol = "tcp"
|
||||
},
|
||||
{
|
||||
internal = 7359
|
||||
external = 7359
|
||||
protocol = "udp"
|
||||
},
|
||||
{
|
||||
internal = 1900
|
||||
external = 1900
|
||||
protocol = "udp"
|
||||
}
|
||||
]
|
||||
|
||||
jellyfin_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/jellyfin"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = "${var.data_root}"
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
jellyfin_devices = [
|
||||
"/dev/dri/:/dev/dri/"
|
||||
]
|
||||
|
||||
jellyfin_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
JELLYFIN_PublishedServerUrl = "${var.hostname}/jellyfin"
|
||||
}
|
||||
}
|
||||
|
||||
module "jellyfin" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.jellyfin_volumes
|
||||
env_vars = local.jellyfin_env_vars
|
||||
ports = local.internal_ports
|
||||
devices = local.jellyfin_devices
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for integration with networking modules"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8096
|
||||
endpoint = "http://${local.container_name}:8096"
|
||||
subdomains = ["jellyfin"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "jellyseerr"
|
||||
image = "fallenbagel/jellyseerr"
|
||||
tag = "latest"
|
||||
|
||||
jellyseerr_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/jellyseerr"
|
||||
container_path = "/app/config"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
jellyseerr_env_vars = {
|
||||
LOG_LEVEL = "debug"
|
||||
TZ = var.timezone
|
||||
}
|
||||
|
||||
jellyseerr_healthcheck = {
|
||||
test = ["CMD", "wget", "http://127.0.0.1:5055/api/v1/status", "-qO", "/dev/null"]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "jellyseerr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.jellyseerr_volumes
|
||||
env_vars = local.jellyseerr_env_vars
|
||||
healthcheck = local.jellyseerr_healthcheck
|
||||
ports = [{
|
||||
internal = 5055
|
||||
external = 5055
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for integration with networking modules"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 5055
|
||||
endpoint = "http://${local.container_name}:5055"
|
||||
subdomains = ["requests"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "prowlarr"
|
||||
image = "lscr.io/linuxserver/prowlarr"
|
||||
tag = "latest"
|
||||
|
||||
prowlarr_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/prowlarr"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
prowlarr_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
}
|
||||
|
||||
prowlarr_healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:9696/prowlarr/ping"]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "prowlarr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.prowlarr_volumes
|
||||
env_vars = local.prowlarr_env_vars
|
||||
healthcheck = local.prowlarr_healthcheck
|
||||
ports = [{
|
||||
internal = 9696
|
||||
external = 9696
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for prowlarr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 9696
|
||||
endpoint = "http://${local.container_name}:9696"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "download_root" {
|
||||
description = "Directory for downloads"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "qbittorrent"
|
||||
image = "lscr.io/linuxserver/qbittorrent"
|
||||
tag = "libtorrentv1"
|
||||
|
||||
qbittorrent_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/qbittorrent"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.download_root
|
||||
container_path = "/data/torrents"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
qbittorrent_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
WEBUI_PORT = "8080"
|
||||
DOCKER_MODS = "ghcr.io/gabe565/linuxserver-mod-vuetorrent"
|
||||
}
|
||||
|
||||
qbittorrent_healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "qbittorrent" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.qbittorrent_volumes
|
||||
env_vars = local.qbittorrent_env_vars
|
||||
healthcheck = local.qbittorrent_healthcheck
|
||||
ports = [{
|
||||
internal = 8080
|
||||
external = 8080
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for qbittorrent"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8080
|
||||
endpoint = "http://${local.container_name}:8080"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "radarr"
|
||||
image = "lscr.io/linuxserver/radarr"
|
||||
tag = "latest"
|
||||
|
||||
radarr_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/radarr"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.data_root
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
radarr_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
}
|
||||
|
||||
radarr_healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:7878/radarr/ping"]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "radarr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.radarr_volumes
|
||||
env_vars = local.radarr_env_vars
|
||||
healthcheck = local.radarr_healthcheck
|
||||
ports = [{
|
||||
internal = 7878
|
||||
external = 7878
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for radarr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 7878
|
||||
endpoint = "http://${local.container_name}:7878"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "readarr"
|
||||
image = "lscr.io/linuxserver/readarr"
|
||||
tag = "develop"
|
||||
|
||||
readarr_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/readarr"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.data_root
|
||||
container_path = "/books"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
readarr_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
}
|
||||
}
|
||||
|
||||
module "readarr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.readarr_volumes
|
||||
env_vars = local.readarr_env_vars
|
||||
ports = [{
|
||||
internal = 8787
|
||||
external = 8787
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for readarr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8787
|
||||
endpoint = "http://${local.container_name}:8787"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "sabnzbd"
|
||||
image = "lscr.io/linuxserver/sabnzbd"
|
||||
tag = "latest"
|
||||
|
||||
sabnzbd_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/sabnzbd/config"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = "${var.data_root}/usenet/downloads"
|
||||
container_path = "/downloads"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
sabnzbd_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
}
|
||||
}
|
||||
|
||||
module "sabnzbd" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.sabnzbd_volumes
|
||||
env_vars = local.sabnzbd_env_vars
|
||||
ports = [{
|
||||
internal = 8080
|
||||
external = 6789
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "unless-stopped"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for integration with networking modules"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8080
|
||||
endpoint = "http://${local.container_name}:8080"
|
||||
subdomains = ["sabnzbd"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "sonarr"
|
||||
image = "lscr.io/linuxserver/sonarr"
|
||||
tag = "latest"
|
||||
|
||||
sonarr_volumes = [
|
||||
{
|
||||
host_path = "${var.volume_path}/sonarr"
|
||||
container_path = "/config"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.data_root
|
||||
container_path = "/data"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
sonarr_env_vars = {
|
||||
PUID = var.user_id
|
||||
PGID = var.group_id
|
||||
TZ = var.timezone
|
||||
}
|
||||
|
||||
sonarr_healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8989/sonarr/ping"]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
start_period = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
module "sonarr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.sonarr_volumes
|
||||
env_vars = local.sonarr_env_vars
|
||||
healthcheck = local.sonarr_healthcheck
|
||||
ports = [{
|
||||
internal = 8989
|
||||
external = 8989
|
||||
protocol = "tcp"
|
||||
}]
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for sonarr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = 8989
|
||||
endpoint = "http://${local.container_name}:8989"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the container"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "download_root" {
|
||||
description = "Directory for downloads"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "sonarr_api_key" {
|
||||
description = "API key for Sonarr"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "radarr_api_key" {
|
||||
description = "API key for Radarr"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Enable container monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "unpackerr"
|
||||
image = "golift/unpackerr"
|
||||
tag = "latest"
|
||||
|
||||
unpackerr_volumes = [
|
||||
{
|
||||
host_path = var.download_root
|
||||
container_path = "/data/torrents"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
unpackerr_env_vars = {
|
||||
TZ = var.timezone
|
||||
UN_SONARR_0_URL = "http://sonarr:8989/sonarr"
|
||||
UN_SONARR_0_API_KEY = var.sonarr_api_key
|
||||
UN_RADARR_0_URL = "http://radarr:7878/radarr"
|
||||
UN_RADARR_0_API_KEY = var.radarr_api_key
|
||||
}
|
||||
|
||||
unpackerr_security_opts = [
|
||||
"no-new-privileges:true"
|
||||
]
|
||||
}
|
||||
|
||||
module "unpackerr" {
|
||||
source = "../../../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.unpackerr_volumes
|
||||
env_vars = local.unpackerr_env_vars
|
||||
security_opts = local.unpackerr_security_opts
|
||||
networks = var.networks
|
||||
monitoring = var.monitoring
|
||||
restart_policy = "always"
|
||||
user = "${var.user_id}:${var.group_id}"
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for unpackerr"
|
||||
value = {
|
||||
name = local.container_name
|
||||
endpoint = "http://${local.container_name}"
|
||||
}
|
||||
}
|
||||
55
modules/20-services-apps/media-server/variables.tf
Normal file
55
modules/20-services-apps/media-server/variables.tf
Normal file
@@ -0,0 +1,55 @@
|
||||
variable "volume_path" {
|
||||
description = "Base directory for volumes (APP_DATA)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "data_root" {
|
||||
description = "Root directory for media data (DATA_ROOT)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "download_root" {
|
||||
description = "Directory for downloads (DOWNLOAD_ROOT)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "user_id" {
|
||||
description = "User ID for container permissions"
|
||||
type = string
|
||||
default = "1000"
|
||||
}
|
||||
|
||||
variable "group_id" {
|
||||
description = "Group ID for container permissions"
|
||||
type = string
|
||||
default = "1000"
|
||||
}
|
||||
|
||||
variable "timezone" {
|
||||
description = "Timezone for the containers"
|
||||
type = string
|
||||
default = "UTC"
|
||||
}
|
||||
|
||||
variable "hostname" {
|
||||
description = "Hostname for the Jellyfin PublishedServerUrl"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "sonarr_api_key" {
|
||||
description = "API key for Sonarr"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "radarr_api_key" {
|
||||
description = "API key for Radarr"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of additional networks to which containers should be attached"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
10
modules/20-services-apps/n8n/.env.example
Normal file
10
modules/20-services-apps/n8n/.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
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=*
|
||||
108
modules/20-services-apps/n8n/README.md
Normal file
108
modules/20-services-apps/n8n/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
13
modules/20-services-apps/n8n/init-data.sh
Normal file
13
modules/20-services-apps/n8n/init-data.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/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
|
||||
134
modules/20-services-apps/n8n/main.tf
Normal file
134
modules/20-services-apps/n8n/main.tf
Normal file
@@ -0,0 +1,134 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
81
modules/20-services-apps/ntfy/README.md
Normal file
81
modules/20-services-apps/ntfy/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
58
modules/20-services-apps/ntfy/main.tf
Normal file
58
modules/20-services-apps/ntfy/main.tf
Normal file
@@ -0,0 +1,58 @@
|
||||
variable "image_tag" {
|
||||
description = "Tag of the ntfy image to use"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Host path for ntfy data volume"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "List of networks to which the container should be attached"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "ntfy"
|
||||
image = "binwiederhier/ntfy"
|
||||
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
monitoring = true
|
||||
exposed_port = 80
|
||||
subdomains = ["ntfy"]
|
||||
default_volumes = [
|
||||
{
|
||||
container_path = "/etc/ntfy"
|
||||
host_path = "${var.volume_path}/app"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
container_path = "/var/cache/ntfy"
|
||||
host_path = "${var.volume_path}/cache"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "ntfy" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.image_tag
|
||||
volumes = local.default_volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.exposed_port
|
||||
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
||||
subdomains = local.subdomains
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# Portainer Module
|
||||
|
||||
This module deploys [Portainer](https://www.portainer.io/), a lightweight management UI that allows you to easily manage your different Docker environments.
|
||||
|
||||
## Overview
|
||||
|
||||
The Portainer module:
|
||||
|
||||
- Deploys one Docker container: `portainer`.
|
||||
- Mounts the Docker socket to allow Portainer to manage the Docker environment.
|
||||
- Persists Portainer data to a volume on the host.
|
||||
- Provides a service definition for integration with networking modules.
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "portainer" {
|
||||
source = "./modules/20-services-apps/portainer"
|
||||
volume_path = "/path/to/volumes/portainer"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------- | ---------------------------------------------------------------- |
|
||||
| `image_tag` | Tag of the Portainer image to use |
|
||||
| `volume_path` | Host path for Portainer data volume |
|
||||
| `networks` | List of additional networks to which Portainer should be attached |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "portainer"
|
||||
primary_port = 9000
|
||||
endpoint = "http://portainer:9000"
|
||||
subdomains = ["portainer"]
|
||||
publish_via = "reverse_proxy"
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Portainer stores its data in a single volume:
|
||||
|
||||
1. Portainer data: `/data` in the container, mapped to `${volume_path}/data` on the host.
|
||||
|
||||
It also mounts the Docker socket from `/var/run/docker.sock` on the host to `/var/run/docker.sock` in the container to manage Docker.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "portainer" {
|
||||
source = "./modules/20-services-apps/portainer"
|
||||
volume_path = "${module.system_globals.volume_host}/portainer"
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.portainer.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
101
modules/20-services-apps/pterodactyl/README.md
Normal file
101
modules/20-services-apps/pterodactyl/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 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/)
|
||||
18
modules/20-services-apps/pterodactyl/panel/.env.example
Normal file
18
modules/20-services-apps/pterodactyl/panel/.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
109
modules/20-services-apps/pterodactyl/panel/README.md
Normal file
109
modules/20-services-apps/pterodactyl/panel/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
164
modules/20-services-apps/pterodactyl/panel/main.tf
Normal file
164
modules/20-services-apps/pterodactyl/panel/main.tf
Normal file
@@ -0,0 +1,164 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
100
modules/20-services-apps/pterodactyl/wings/README.md
Normal file
100
modules/20-services-apps/pterodactyl/wings/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
118
modules/20-services-apps/pterodactyl/wings/main.tf
Normal file
118
modules/20-services-apps/pterodactyl/wings/main.tf
Normal file
@@ -0,0 +1,118 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
79
modules/20-services-apps/searxng/README.md
Normal file
79
modules/20-services-apps/searxng/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# SearxNG Module
|
||||
|
||||
This module deploys [SearxNG](https://searx.github.io/searx/), a privacy-respecting metasearch engine, as a Docker container in the homelab environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The SearxNG module:
|
||||
|
||||
- Deploys the `searxng/searxng` Docker container
|
||||
- Persists configuration data to a volume on the host
|
||||
- Provides service definition for integration with networking modules
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "searxng" {
|
||||
source = "./modules/20-services-apps/searxng"
|
||||
volume_path = "/path/to/volumes/searxng"
|
||||
networks = ["homelab-network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||
| `image_tag` | Tag of the SearxNG image to use | `string` | `"latest"` |
|
||||
| `volume_path` | Host path for SearxNG configuration volume | `string` | - |
|
||||
| `networks` | List of networks to which the container should be attached | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | --------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "searxng"
|
||||
primary_port = 8080
|
||||
endpoint = "http://searxng:8080"
|
||||
subdomains = ["search"]
|
||||
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
SearxNG stores its configuration in a single volume:
|
||||
|
||||
- Configuration: `/etc/searxng` in the container, mapped to `${volume_path}/config` on the host
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
module "searxng" {
|
||||
source = "./modules/20-services-apps/searxng"
|
||||
volume_path = module.system_globals.volume_host
|
||||
networks = [module.services.homelab_docker_network_name]
|
||||
}
|
||||
|
||||
# The service definition is automatically included in the services output
|
||||
module "services" {
|
||||
source = "./modules/services"
|
||||
# ...
|
||||
service_definitions = [
|
||||
module.searxng.service_definition,
|
||||
# Other service definitions
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,13 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = {
|
||||
source = "germanbrew/dotenv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "The tag for the portainer container image"
|
||||
description = "The tag for the searxng container image"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
@@ -17,30 +24,21 @@ variable "networks" {
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "portainer"
|
||||
image = "portainer/portainer-ce"
|
||||
tag = var.image_tag
|
||||
container_name = "searxng"
|
||||
image = "searxng/searxng"
|
||||
tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||
monitoring = true
|
||||
internal_port = 9000
|
||||
exposed_port = 9000
|
||||
|
||||
# Define volumes
|
||||
internal_port = 8080
|
||||
volumes = [
|
||||
{
|
||||
host_path = "/var/run/docker.sock"
|
||||
container_path = "/var/run/docker.sock"
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = "${var.volume_path}/data"
|
||||
container_path = "/data"
|
||||
host_path = "${var.volume_path}/config"
|
||||
container_path = "/etc/searxng"
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Create the portainer container
|
||||
module "portainer" {
|
||||
module "searxng" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
@@ -48,20 +46,15 @@ module "portainer" {
|
||||
volumes = local.volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [
|
||||
{
|
||||
internal = local.internal_port
|
||||
external = local.exposed_port
|
||||
protocol = "tcp"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "General service definition with optional ingress configuration"
|
||||
description = "Service definition with ingress configuration"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
subdomains = ["search"]
|
||||
publish_via = "tunnel"
|
||||
}
|
||||
}
|
||||
72
services/main.tf
Executable file → Normal file
72
services/main.tf
Executable file → Normal file
@@ -1,8 +1,6 @@
|
||||
locals {
|
||||
module_dir = "../modules"
|
||||
root_volume = module.system_globals.volume_host
|
||||
volume_host = "${module.system_globals.volume_host}/appdata"
|
||||
data_host = "${module.system_globals.volume_host}/data"
|
||||
volume_host = module.system_globals.volume_host
|
||||
}
|
||||
|
||||
module "system_globals" {
|
||||
@@ -20,14 +18,16 @@ module "homelab_docker_network" {
|
||||
subnet = "10.100.0.0/16"
|
||||
}
|
||||
|
||||
// Docker network used for media services
|
||||
module "media_docker_network" {
|
||||
source = "${local.module_dir}/01-networking/docker-network"
|
||||
module "actualbudget" {
|
||||
source = "${local.module_dir}/20-services-apps/actualbudget"
|
||||
volume_path = "${local.volume_host}/actual"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
name = "media-network"
|
||||
driver = "bridge"
|
||||
attachable = true
|
||||
subnet = "10.110.0.0/16"
|
||||
module "affine" {
|
||||
source = "${local.module_dir}/20-services-apps/affine"
|
||||
volume_path = "${local.volume_host}/affine"
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
module "calibre" {
|
||||
@@ -36,25 +36,9 @@ module "calibre" {
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
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 "emulatorjs" {
|
||||
source = "${local.module_dir}/20-services-apps/emulatorjs"
|
||||
volume_path = "${local.volume_host}/emulatorjs"
|
||||
}
|
||||
|
||||
module "linkwarden" {
|
||||
@@ -63,10 +47,32 @@ module "linkwarden" {
|
||||
networks = [module.homelab_docker_network.name]
|
||||
}
|
||||
|
||||
|
||||
module "portainer" {
|
||||
source = "${local.module_dir}/20-services-apps/portainer"
|
||||
volume_path = "${local.volume_host}/portainer"
|
||||
module "ntfy" {
|
||||
source = "${local.module_dir}/20-services-apps/ntfy"
|
||||
volume_path = "${local.volume_host}/ntfy"
|
||||
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
Executable file → Normal file
12
services/outputs.tf
Executable file → Normal file
@@ -4,12 +4,16 @@
|
||||
output "service_definitions" {
|
||||
description = "Service definitions for all services"
|
||||
value = [
|
||||
module.actualbudget.service_definition,
|
||||
module.affine.service_definition,
|
||||
module.calibre.service_definition,
|
||||
module.glance.service_definition,
|
||||
module.immich.service_definition,
|
||||
module.jellyfin.service_definition,
|
||||
module.emulatorjs.service_definition,
|
||||
module.linkwarden.service_definition,
|
||||
module.portainer.service_definition
|
||||
module.ntfy.service_definition,
|
||||
module.pterodactyl_wings.service_definition,
|
||||
module.pterodactyl_panel.service_definition,
|
||||
module.n8n.service_definition,
|
||||
module.searxng.service_definition
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user