23 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -144,10 +144,31 @@ resource "docker_container" "service_container" {
hostname = var.hostname hostname = var.hostname
domainname = var.domainname domainname = var.domainname
user = var.user user = var.user
group_add = var.group_add
working_dir = var.working_dir working_dir = var.working_dir
command = var.command command = var.command
entrypoint = var.entrypoint entrypoint = var.entrypoint
privileged = var.privileged privileged = var.privileged
destroy_grace_seconds = var.destroy_grace_seconds
# Linux capabilities controls
dynamic "capabilities" {
for_each = length(var.capabilities_add) > 0 || length(var.capabilities_drop) > 0 ? [1] : []
content {
add = var.capabilities_add
drop = var.capabilities_drop
}
}
# Device mappings
dynamic "devices" {
for_each = var.devices
content {
host_path = devices.value.host_path
container_path = devices.value.container_path
permissions = devices.value.permissions
}
}
# Set log options # Set log options
log_driver = var.log_driver log_driver = var.log_driver

View File

@@ -179,12 +179,48 @@ variable "entrypoint" {
default = null default = null
} }
variable "group_add" {
description = "Additional groups to add to the container"
type = list(string)
default = []
}
variable "privileged" { variable "privileged" {
description = "Run container in privileged mode" description = "Run container in privileged mode"
type = bool type = bool
default = false 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 // Logging options
variable "log_driver" { variable "log_driver" {
description = "Log driver for the container" description = "Log driver for the container"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
POSTGRES_USER=admin
POSTGRES_PASSWORD=
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=
POSTGRES_NON_ROOT_PASSWORD=
N8N_HOST=localhost
N8N_PORT=5678
N8N_PROTOCOL=http
WEBHOOK_URL=https://n8n.yourdomain.com/
NODE_FUNCTION_ALLOW_EXTERNAL=*

View File

@@ -1,108 +0,0 @@
# n8n Module
This module deploys [n8n](https://n8n.io/), a workflow automation tool for technical people, as Docker containers in the homelab environment.
## Overview
The n8n module:
- Deploys two Docker containers:
- `n8n`: The main workflow automation server
- `n8n-postgres`: A PostgreSQL database backend
- Creates a dedicated Docker network (`n8n-network`) for container communication
- Persists data to volumes on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "n8n" {
source = "./modules/20-services-apps/n8n"
volume_path = "/path/to/volumes/n8n"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the n8n image to use | `string` | `"latest"` |
| `volume_path` | Host path for n8n and Postgres data volumes | `string` | - |
| `networks` | List of additional networks to which n8n should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | ---------------------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "n8n"
primary_port = 5678
endpoint = "http://n8n:5678"
subdomains = ["n8n"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Environment Variables
n8n requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
- Database configuration:
- `POSTGRES_USER`: Root PostgreSQL user
- `POSTGRES_PASSWORD`: Root PostgreSQL password
- `POSTGRES_DB`: Database name for n8n
- `POSTGRES_NON_ROOT_USER`: Non-root user for n8n to connect with
- `POSTGRES_NON_ROOT_PASSWORD`: Password for the non-root user
- n8n configuration:
- `N8N_HOST`: Host for n8n to use
- `N8N_PORT`: Port for n8n to use
- `N8N_PROTOCOL`: Protocol for n8n (http or https)
- `WEBHOOK_URL`: URL for webhooks
- `NODE_FUNCTION_ALLOW_EXTERNAL`: Whether to allow external function calls
## Data Persistence
n8n stores its data in two main volumes:
1. n8n application data: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
Additionally, an initialization script is mounted to the PostgreSQL container:
- `/docker-entrypoint-initdb.d/init-data.sh` in the container, from `${volume_path}/init-data.sh` on the host
## Networking
The module creates a dedicated Docker network named `n8n-network` for communication between the n8n and PostgreSQL containers. The n8n container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
## Integration with Networking Modules
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
## Example Integration in Main Configuration
```hcl
module "n8n" {
source = "./modules/20-services-apps/n8n"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.n8n.service_definition,
# Other service definitions
]
}
```

View File

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

View File

@@ -1,134 +0,0 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the n8n container image"
type = string
default = "latest"
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
default = []
}
locals {
container_name = "n8n"
database_name = "n8n-postgres"
n8n_image = "docker.n8n.io/n8nio/n8n"
database_image = "postgres"
n8n_tag = var.image_tag != "" ? var.image_tag : "latest"
database_tag = "16"
monitoring = true
env_file = "${path.module}/.env"
n8n_internal_port = 5678
# Define volumes
n8n_volumes = [
{
host_path = "${var.volume_path}/n8n_storage/_data"
container_path = "/home/node/.n8n"
read_only = false
}
]
database_volumes = [
{
host_path = "${var.volume_path}/db_storage/_data"
container_path = "/var/lib/postgresql/data"
read_only = false
},
{
host_path = "${var.volume_path}/init-data.sh"
container_path = "/docker-entrypoint-initdb.d/init-data.sh"
read_only = false
}
]
# Environment variables for the database
database_env_vars = {
POSTGRES_USER = provider::dotenv::get_by_key("POSTGRES_USER", local.env_file)
POSTGRES_PASSWORD = provider::dotenv::get_by_key("POSTGRES_PASSWORD", local.env_file)
POSTGRES_DB = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
POSTGRES_NON_ROOT_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
POSTGRES_NON_ROOT_PASSWORD = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_PASSWORD", local.env_file)
}
# Environment variables for n8n
n8n_env_vars = {
DB_TYPE = "postgresdb"
DB_POSTGRESDB_HOST = local.database_name
DB_POSTGRESDB_PORT = 5432
DB_POSTGRESDB_DATABASE = provider::dotenv::get_by_key("POSTGRES_DB", local.env_file)
DB_POSTGRESDB_USER = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_USER", local.env_file)
DB_POSTGRESDB_PASSWORD = provider::dotenv::get_by_key("POSTGRES_NON_ROOT_PASSWORD", local.env_file)
N8N_HOST = provider::dotenv::get_by_key("N8N_HOST", local.env_file)
N8N_PORT = provider::dotenv::get_by_key("N8N_PORT", local.env_file)
N8N_PROTOCOL = provider::dotenv::get_by_key("N8N_PROTOCOL", local.env_file)
WEBHOOK_URL = provider::dotenv::get_by_key("WEBHOOK_URL", local.env_file)
NODE_FUNCTION_ALLOW_EXTERNAL = provider::dotenv::get_by_key("NODE_FUNCTION_ALLOW_EXTERNAL", local.env_file)
}
# Healthcheck configuration for the database
database_healthcheck = {
test = ["CMD-SHELL", "pg_isready -h localhost -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval = "5s"
timeout = "5s"
retries = 10
start_period = "10s"
}
}
module "n8n_network" {
source = "../../01-networking/docker-network"
name = "n8n-network"
driver = "bridge"
}
# Create the PostgreSQL container
module "postgres" {
source = "../../10-services-generic/docker-service"
container_name = local.database_name
image = local.database_image
tag = local.database_tag
volumes = local.database_volumes
env_vars = local.database_env_vars
networks = [module.n8n_network.name]
monitoring = local.monitoring
healthcheck = local.database_healthcheck
}
# Create the n8n container
module "n8n" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = local.n8n_image
tag = local.n8n_tag
volumes = local.n8n_volumes
env_vars = local.n8n_env_vars
networks = concat([module.n8n_network.name], var.networks)
monitoring = local.monitoring
depends_on = [module.postgres]
}
output "service_definition" {
description = "General service definition with optional ingress configuration"
value = {
name = local.container_name
primary_port = local.n8n_internal_port
endpoint = "http://${local.container_name}:${local.n8n_internal_port}"
subdomains = ["n8n"]
publish_via = "tunnel"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,79 +0,0 @@
# SearxNG Module
This module deploys [SearxNG](https://searx.github.io/searx/), a privacy-respecting metasearch engine, as a Docker container in the homelab environment.
## Overview
The SearxNG module:
- Deploys the `searxng/searxng` Docker container
- Persists configuration data to a volume on the host
- Provides service definition for integration with networking modules
## Usage
```hcl
module "searxng" {
source = "./modules/20-services-apps/searxng"
volume_path = "/path/to/volumes/searxng"
networks = ["homelab-network"]
}
```
## Variables
| Variable | Description | Type | Default |
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
| `image_tag` | Tag of the SearxNG image to use | `string` | `"latest"` |
| `volume_path` | Host path for SearxNG configuration volume | `string` | - |
| `networks` | List of networks to which the container should be attached | `list(string)` | `[]` |
## Outputs
| Output | Description |
| -------------------- | --------------------------------------------- |
| `service_definition` | Service definition for integration with networking modules |
## Service Definition
This module outputs a service definition that is used by the networking modules to expose the service.
```hcl
{
name = "searxng"
primary_port = 8080
endpoint = "http://searxng:8080"
subdomains = ["search"]
publish_via = "tunnel" # Only publish through Cloudflare tunnel
}
```
## Data Persistence
SearxNG stores its configuration in a single volume:
- Configuration: `/etc/searxng` in the container, mapped to `${volume_path}/config` on the host
## Integration with Networking Modules
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
## Example Integration in Main Configuration
```hcl
module "searxng" {
source = "./modules/20-services-apps/searxng"
volume_path = module.system_globals.volume_host
networks = [module.services.homelab_docker_network_name]
}
# The service definition is automatically included in the services output
module "services" {
source = "./modules/services"
# ...
service_definitions = [
module.searxng.service_definition,
# Other service definitions
]
}
```

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

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

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

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