Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e52d667cab | |||
| 0fe34fb0e4 | |||
|
|
bce43c4a71 | ||
|
|
60e3a41ac5 | ||
|
|
b9301fff36 | ||
|
|
80f8857dd2 | ||
|
|
2c8c43ff68 | ||
|
|
4edfd642f3 | ||
|
|
c59ebbcc8b | ||
|
|
8ee71193bb | ||
|
|
4f5ee19cef | ||
|
|
9c46aa0d5b | ||
|
|
0a076a9af5 | ||
|
|
6595de4788 | ||
|
|
3725c73bce | ||
|
|
eefe369975 | ||
|
|
ede6c52a40 | ||
|
|
a63f144bf1 | ||
|
|
82d8ca0463 | ||
|
|
c02ac6f961 | ||
|
|
b03034b742 | ||
|
|
26808e4ca6 | ||
|
|
5cd8d36d97 | ||
|
|
9d5b083b32 | ||
|
|
b73c7ab21d | ||
|
|
b5832c27a9 | ||
|
|
8d5008c6ca | ||
|
|
6709c85b0e | ||
|
|
e4ecd6dbcf | ||
|
|
ec326cd75c | ||
|
|
1ab62c834c | ||
|
|
45fc919f6d | ||
|
|
4943c7c41b | ||
|
|
715bcfbd7c | ||
|
|
af038e23ea | ||
|
|
d801b0b86d |
@@ -3,6 +3,7 @@ TIMEZONE="Australia/Brisbane"
|
|||||||
DATA_DIR="/mnt/appdata"
|
DATA_DIR="/mnt/appdata"
|
||||||
PUID="1000"
|
PUID="1000"
|
||||||
PGID="1000"
|
PGID="1000"
|
||||||
|
EXTERNAL_IP=your-public-ip-address
|
||||||
|
|
||||||
# Cloudflare
|
# Cloudflare
|
||||||
CLOUDFLARE_API_TOKEN="your-cloudflare-api-token"
|
CLOUDFLARE_API_TOKEN="your-cloudflare-api-token"
|
||||||
|
|||||||
17
.terraform.lock.hcl
generated
17
.terraform.lock.hcl
generated
@@ -47,6 +47,23 @@ provider "registry.opentofu.org/germanbrew/dotenv" {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider "registry.opentofu.org/hashicorp/local" {
|
||||||
|
version = "2.5.3"
|
||||||
|
hashes = [
|
||||||
|
"h1:mC9+u1eaUILTjxey6Ivyf/3djm//RNNze9kBVX/trng=",
|
||||||
|
"zh:32e1d4b0595cea6cda4ca256195c162772ddff25594ab4008731a2ec7be230bf",
|
||||||
|
"zh:48c390af0c87df994ec9796f04ec2582bcac581fb81ed6bb58e0671da1c17991",
|
||||||
|
"zh:4be7289c969218a57b40902e2f359914f8d35a7f97b439140cb711aa21e494bd",
|
||||||
|
"zh:4cf958e631e99ed6c8b522c9b22e1f1b568c0bdadb01dd002ca7dffb1c927764",
|
||||||
|
"zh:7a0132c0faca4c4c96aa70808effd6817e28712bf5a39881666ac377b4250acf",
|
||||||
|
"zh:7d60de08fac427fb045e4590d1b921b6778498eee9eb16f78c64d4c577bde096",
|
||||||
|
"zh:91003bee5981e99ec3925ce2f452a5f743827f9d0e131a86613549c1464796f0",
|
||||||
|
"zh:9fe2fe75977c8149e2515fb30c6cc6cfd57b225d4ce592c570d81a3831d7ffa3",
|
||||||
|
"zh:e210e6be54933ce93e03d0994e520ba289aa01b2c1f70e77afb8f2ee796b0fe3",
|
||||||
|
"zh:e8793e5f9422f2b31a804e51806595f335b827c9a38db18766960464566f21d5",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
provider "registry.opentofu.org/hashicorp/random" {
|
provider "registry.opentofu.org/hashicorp/random" {
|
||||||
version = "3.5.1"
|
version = "3.5.1"
|
||||||
constraints = "~> 3.5.1"
|
constraints = "~> 3.5.1"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
26
main.tf
26
main.tf
@@ -1,7 +1,15 @@
|
|||||||
|
module "system_globals" {
|
||||||
|
source = "./modules/00-globals/system"
|
||||||
|
}
|
||||||
|
|
||||||
module "cloudflare_globals" {
|
module "cloudflare_globals" {
|
||||||
source = "./modules/00-globals/cloudflare"
|
source = "./modules/00-globals/cloudflare"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module "tls_globals" {
|
||||||
|
source = "./modules/00-globals/tls"
|
||||||
|
}
|
||||||
|
|
||||||
module "watchtower" {
|
module "watchtower" {
|
||||||
source = "./modules/20-services-apps/watchtower"
|
source = "./modules/20-services-apps/watchtower"
|
||||||
}
|
}
|
||||||
@@ -11,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
|
||||||
@@ -22,3 +34,17 @@ module "homelab_cloudflared_tunnel" {
|
|||||||
networks = [module.services.homelab_docker_network_name]
|
networks = [module.services.homelab_docker_network_name]
|
||||||
monitoring = true
|
monitoring = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module "homelab_caddy_proxy" {
|
||||||
|
source = "./modules/01-networking/caddy-proxy"
|
||||||
|
|
||||||
|
domain = module.cloudflare_globals.domain
|
||||||
|
tls_email = module.tls_globals.tls_email
|
||||||
|
container_name = "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
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
monitoring = true
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,3 +30,8 @@ output "cloudflare_api_token" {
|
|||||||
value = data.dotenv_sensitive.cloudflare_credentials.entries.CLOUDFLARE_API_TOKEN
|
value = data.dotenv_sensitive.cloudflare_credentials.entries.CLOUDFLARE_API_TOKEN
|
||||||
sensitive = true
|
sensitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "external_ip" {
|
||||||
|
description = "External IP address for the homelab"
|
||||||
|
value = data.dotenv.cloudflare_config.entries.EXTERNAL_IP
|
||||||
|
}
|
||||||
|
|||||||
15
modules/00-globals/tls/main.tf
Normal file
15
modules/00-globals/tls/main.tf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
dotenv = {
|
||||||
|
source = "germanbrew/dotenv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "dotenv" "system_config" {}
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
output "tls_email" {
|
||||||
|
description = "TLS email"
|
||||||
|
value = data.dotenv.system_config.entries.TLS_EMAIL
|
||||||
|
}
|
||||||
147
modules/01-networking/caddy-proxy/README.md
Normal file
147
modules/01-networking/caddy-proxy/README.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Caddy Proxy Module
|
||||||
|
|
||||||
|
This module creates a Caddy reverse proxy server that dynamically configures itself based on service definitions passed to it.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Caddy Proxy module:
|
||||||
|
- Accepts service definitions that specify whether to expose them via reverse proxy
|
||||||
|
- Dynamically generates Caddyfile configuration from these service definitions
|
||||||
|
- Supports custom Caddy configuration blocks per service
|
||||||
|
- Deploys a Caddy container with the generated configuration
|
||||||
|
- Manages TLS certificates automatically using Let's Encrypt
|
||||||
|
- Creates DNS records for services with configurable Cloudflare proxying settings
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Integration
|
||||||
|
|
||||||
|
Add the module to your main Terraform configuration:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "homelab_caddy_proxy" {
|
||||||
|
source = "./modules/01-networking/caddy-proxy"
|
||||||
|
domain = "yourdomain.com"
|
||||||
|
tls_email = "your-email@example.com" # For Let's Encrypt
|
||||||
|
container_name = "caddy-proxy"
|
||||||
|
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
|
||||||
|
external_ip = module.cloudflare_globals.external_ip
|
||||||
|
service_definitions = module.services.service_definitions
|
||||||
|
networks = ["your-docker-network"]
|
||||||
|
monitoring = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Definition Format
|
||||||
|
|
||||||
|
Services should include the following fields to be properly exposed through Caddy:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "service-name"
|
||||||
|
endpoint = "service-container:port"
|
||||||
|
subdomains = ["app", "dashboard"] # Will create app.yourdomain.com, dashboard.yourdomain.com
|
||||||
|
|
||||||
|
# Specify how to publish this service: "tunnel", "reverse_proxy", or "both" (default)
|
||||||
|
publish_via = "both"
|
||||||
|
|
||||||
|
# Control whether the DNS record is proxied through Cloudflare (default: true)
|
||||||
|
proxied = true
|
||||||
|
|
||||||
|
# Option 1: Simplified Caddy configuration via options
|
||||||
|
caddy_options = {
|
||||||
|
"health_path" = "/health"
|
||||||
|
"health_interval" = "30s"
|
||||||
|
"header_up X-Real-IP" = "{http.request.remote}"
|
||||||
|
# Additional reverse_proxy options as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Option 2: Full custom Caddy configuration (takes precedence if both are provided)
|
||||||
|
caddy_config = <<-EOT
|
||||||
|
# Raw Caddy configuration goes here
|
||||||
|
reverse_proxy /api/* api-backend:8080
|
||||||
|
reverse_proxy /* frontend:3000
|
||||||
|
header X-Powered-By "My Awesome Homelab"
|
||||||
|
log {
|
||||||
|
output file /var/log/access.log
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `publish_via` field controls which networking module(s) will expose the service:
|
||||||
|
- `"tunnel"`: Service will only be published via Cloudflare tunnel
|
||||||
|
- `"reverse_proxy"`: Service will only be exposed via Caddy reverse proxy
|
||||||
|
- `"both"`: Service will be published via both methods (default)
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
|----------|-------------|------|---------|
|
||||||
|
| `container_name` | The name of the Caddy container | `string` | `""` (generates "caddy-proxy") |
|
||||||
|
| `image_tag` | The tag of the Caddy Docker image to use | `string` | `"latest"` |
|
||||||
|
| `domain` | The domain name to use for services | `string` | - |
|
||||||
|
| `tls_email` | Email address for Let's Encrypt | `string` | - |
|
||||||
|
| `service_definitions` | List of service definitions to evaluate | `list(object)` | - |
|
||||||
|
| `networks` | List of Docker networks to connect to | `list(string)` | `[]` |
|
||||||
|
| `monitoring` | Whether to enable monitoring for the container | `bool` | `false` |
|
||||||
|
| `cloudflare_zone_id` | Cloudflare Zone ID for creating DNS records | `string` | `""` |
|
||||||
|
| `external_ip` | External IP address for A records | `string` | `""` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `container_name` | The name of the deployed Caddy container |
|
||||||
|
| `config_hash` | The SHA256 hash of the generated Caddyfile content |
|
||||||
|
| `service_sites` | Map of generated Caddy site configurations |
|
||||||
|
|
||||||
|
## Example Service Integration
|
||||||
|
|
||||||
|
### Basic Service with Default Settings
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Example based on jellyfin (reverse-proxy only with direct IP exposure)
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for a media server"
|
||||||
|
value = {
|
||||||
|
name = "jellyfin"
|
||||||
|
primary_port = 8096
|
||||||
|
endpoint = "http://jellyfin:8096"
|
||||||
|
subdomains = ["media"]
|
||||||
|
publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy
|
||||||
|
proxied = false # Don't proxy through Cloudflare (expose direct IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service with Custom Caddy Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Example showing a service with custom Caddy configuration
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition with custom Caddy configuration"
|
||||||
|
value = {
|
||||||
|
name = "custom-service"
|
||||||
|
primary_port = 8080
|
||||||
|
endpoint = "http://custom-service:8080"
|
||||||
|
subdomains = ["custom"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = true # Use Cloudflare proxying (default)
|
||||||
|
caddy_config = <<-EOT
|
||||||
|
# Handle API requests specially
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy custom-service:8080 {
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle all other requests
|
||||||
|
handle {
|
||||||
|
reverse_proxy custom-service:8080
|
||||||
|
header +Access-Control-Allow-Origin "*"
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
127
modules/01-networking/caddy-proxy/main.tf
Normal file
127
modules/01-networking/caddy-proxy/main.tf
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
docker = {
|
||||||
|
source = "kreuzwerker/docker"
|
||||||
|
}
|
||||||
|
cloudflare = {
|
||||||
|
source = "cloudflare/cloudflare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = var.container_name != "" ? var.container_name : "caddy-proxy"
|
||||||
|
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||||
|
|
||||||
|
// Filter services to only include those that should be published via reverse proxy
|
||||||
|
proxy_services = [
|
||||||
|
for service in var.service_definitions :
|
||||||
|
service if length(service.subdomains) > 0 && (service.publish_via == "reverse_proxy" || service.publish_via == "both")
|
||||||
|
]
|
||||||
|
|
||||||
|
// Transform service definitions into Caddyfile blocks
|
||||||
|
caddy_site_configs = flatten([
|
||||||
|
for service in local.proxy_services :
|
||||||
|
[
|
||||||
|
for subdomain in service.subdomains : {
|
||||||
|
site_address = "${subdomain}.${var.domain}"
|
||||||
|
endpoint = service.endpoint
|
||||||
|
service_name = service.name
|
||||||
|
tls_email = var.tls_email
|
||||||
|
has_custom_config = service.caddy_config != ""
|
||||||
|
custom_config = service.caddy_config
|
||||||
|
reverse_proxy_options = service.caddy_options
|
||||||
|
proxied = service.proxied
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
// Generate the main Caddyfile content
|
||||||
|
caddyfile_content = join("\n\n", [
|
||||||
|
for site in local.caddy_site_configs :
|
||||||
|
site.has_custom_config ?
|
||||||
|
// Use the custom Caddy config if provided
|
||||||
|
<<-EOT
|
||||||
|
${site.site_address} {
|
||||||
|
tls ${var.tls_email}
|
||||||
|
${site.custom_config}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
:
|
||||||
|
// Otherwise use the standard reverse proxy config with options
|
||||||
|
<<-EOT
|
||||||
|
${site.site_address} {
|
||||||
|
tls ${var.tls_email}
|
||||||
|
reverse_proxy ${site.endpoint} {
|
||||||
|
${join("\n ", [
|
||||||
|
for key, value in site.reverse_proxy_options :
|
||||||
|
"${key} ${value}"
|
||||||
|
])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Caddyfile in the volume path
|
||||||
|
resource "local_file" "caddyfile" {
|
||||||
|
content = local.caddyfile_content
|
||||||
|
filename = "${var.volume_path}/caddy/Caddyfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "dns_records" {
|
||||||
|
count = var.cloudflare_zone_id != "" ? 1 : 0
|
||||||
|
source = "../../10-services-generic/cloudflare-dns"
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
dns_records = {
|
||||||
|
for site in local.caddy_site_configs : site.site_address => {
|
||||||
|
name = site.site_address
|
||||||
|
value = var.external_ip
|
||||||
|
type = "A"
|
||||||
|
proxied = site.proxied
|
||||||
|
ttl = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "caddy" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
|
||||||
|
container_name = local.container_name
|
||||||
|
image = "caddy"
|
||||||
|
tag = local.image_tag
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/caddy/data"
|
||||||
|
container_path = "/data"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/caddy/config"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/caddy/Caddyfile"
|
||||||
|
container_path = "/etc/caddy/Caddyfile"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ports = [
|
||||||
|
{
|
||||||
|
external = "80"
|
||||||
|
internal = "80"
|
||||||
|
protocol = "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
external = "443"
|
||||||
|
internal = "443"
|
||||||
|
protocol = "tcp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
monitoring = var.monitoring
|
||||||
|
networks = var.networks
|
||||||
|
}
|
||||||
16
modules/01-networking/caddy-proxy/outputs.tf
Normal file
16
modules/01-networking/caddy-proxy/outputs.tf
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
output "container_name" {
|
||||||
|
description = "The name of the deployed Caddy container"
|
||||||
|
value = module.caddy.container_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "config_hash" {
|
||||||
|
description = "The SHA256 hash of the generated Caddyfile content"
|
||||||
|
value = sha256(local.caddyfile_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_sites" {
|
||||||
|
description = "Map of generated Caddy site configurations"
|
||||||
|
value = {
|
||||||
|
for site in local.caddy_site_configs : site.site_address => site.endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
63
modules/01-networking/caddy-proxy/variables.tf
Normal file
63
modules/01-networking/caddy-proxy/variables.tf
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
variable "container_name" {
|
||||||
|
description = "The name of the Caddy container"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image_tag" {
|
||||||
|
description = "The tag of the Caddy Docker image to use"
|
||||||
|
type = string
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain" {
|
||||||
|
description = "The domain name to use for services"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tls_email" {
|
||||||
|
description = "Email address to use for TLS certificate generation with Let's Encrypt"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "service_definitions" {
|
||||||
|
description = "List of service definitions to evaluate for exposure through Caddy"
|
||||||
|
type = list(object({
|
||||||
|
name = string
|
||||||
|
endpoint = string
|
||||||
|
subdomains = optional(list(string), [])
|
||||||
|
publish_via = optional(string)
|
||||||
|
caddy_config = optional(string, "")
|
||||||
|
caddy_options = optional(map(string), {})
|
||||||
|
proxied = optional(bool, true) # Controls whether the DNS record is proxied through Cloudflare
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of Docker networks to connect the Caddy container to"
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Whether to enable monitoring for the Caddy container"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_zone_id" {
|
||||||
|
description = "Cloudflare Zone ID for creating DNS records"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "external_ip" {
|
||||||
|
description = "External IP address for A records when using create_dns_records"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,21 @@ resource "cloudflare_zero_trust_tunnel_cloudflared" "this" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
|
// Filter services to only include those that should be published via tunnel
|
||||||
|
tunnel_services = [
|
||||||
|
for service in var.service_definitions :
|
||||||
|
service if length(service.subdomains) > 0 && (service.publish_via == "tunnel" || service.publish_via == "both")
|
||||||
|
]
|
||||||
|
|
||||||
// Transform service definitions into ingress rules format, only for services with ingress_enabled
|
// Transform service definitions into ingress rules format, only for services with ingress_enabled
|
||||||
service_ingress_rules = flatten([
|
service_ingress_rules = flatten([
|
||||||
for service in var.service_definitions :
|
for service in local.tunnel_services :
|
||||||
// Only process services with subdomains AND where ingress is enabled (or default to true for backward compatibility)
|
[
|
||||||
(length(service.subdomains) > 0) ? [
|
|
||||||
for subdomain in service.subdomains : {
|
for subdomain in service.subdomains : {
|
||||||
hostname = "${subdomain}.${var.domain}"
|
hostname = "${subdomain}.${var.domain}"
|
||||||
service = service.endpoint
|
service = service.endpoint
|
||||||
}
|
}
|
||||||
] : []
|
]
|
||||||
])
|
])
|
||||||
|
|
||||||
// Combine manual ingress rules and service-generated ones
|
// Combine manual ingress rules and service-generated ones
|
||||||
@@ -67,21 +72,18 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "this" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create DNS record for each service
|
module "dns_records" {
|
||||||
resource "cloudflare_record" "service" {
|
source = "../../10-services-generic/cloudflare-dns"
|
||||||
for_each = {
|
|
||||||
for rule in local.all_ingress_rules : rule.hostname => rule
|
|
||||||
if rule.hostname != null && rule.hostname != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
zone_id = var.cloudflare_zone_id
|
zone_id = var.cloudflare_zone_id
|
||||||
name = split(".", each.value.hostname)[0] // Extract subdomain
|
hostnames = [
|
||||||
content = "${cloudflare_zero_trust_tunnel_cloudflared.this.id}.cfargotunnel.com"
|
for rule in local.all_ingress_rules :
|
||||||
type = "CNAME"
|
rule.hostname if rule.hostname != null && rule.hostname != ""
|
||||||
proxied = true
|
]
|
||||||
|
target_content = "${cloudflare_zero_trust_tunnel_cloudflared.this.id}.cfargotunnel.com"
|
||||||
|
record_type = "CNAME"
|
||||||
|
proxied = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the Docker container
|
|
||||||
locals {
|
locals {
|
||||||
container_name = var.container_name != "" ? var.container_name : "cloudflared-${var.tunnel_name}"
|
container_name = var.container_name != "" ? var.container_name : "cloudflared-${var.tunnel_name}"
|
||||||
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
image_tag = var.image_tag != "" ? var.image_tag : "latest"
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ output "cname_target" {
|
|||||||
value = "${cloudflare_zero_trust_tunnel_cloudflared.this.id}.cfargotunnel.com"
|
value = "${cloudflare_zero_trust_tunnel_cloudflared.this.id}.cfargotunnel.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "dns_records" {
|
|
||||||
description = "Map of created DNS records"
|
|
||||||
value = { for k, v in cloudflare_record.service : k => v.hostname }
|
|
||||||
}
|
|
||||||
|
|
||||||
output "container_name" {
|
output "container_name" {
|
||||||
description = "The name of the Cloudflared tunnel container"
|
description = "The name of the Cloudflared tunnel container"
|
||||||
value = module.cloudflared.container_name
|
value = module.cloudflared.container_name
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ variable "service_definitions" {
|
|||||||
name = string
|
name = string
|
||||||
primary_port = number
|
primary_port = number
|
||||||
endpoint = string
|
endpoint = string
|
||||||
subdomains = optional(list(string), [])
|
subdomains = optional(list(string), [])
|
||||||
|
publish_via = optional(string)
|
||||||
}))
|
}))
|
||||||
default = []
|
default = []
|
||||||
}
|
}
|
||||||
|
|||||||
33
modules/10-services-generic/cloudflare-dns/main.tf
Normal file
33
modules/10-services-generic/cloudflare-dns/main.tf
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
cloudflare = {
|
||||||
|
source = "cloudflare/cloudflare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
hostname_records = length(var.hostnames) > 0 ? {
|
||||||
|
for hostname in var.hostnames :
|
||||||
|
hostname => {
|
||||||
|
name = split(".", hostname)[0] // Extract subdomain
|
||||||
|
value = var.target_content
|
||||||
|
type = var.record_type
|
||||||
|
proxied = var.proxied
|
||||||
|
ttl = var.ttl
|
||||||
|
}
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
all_records = merge(local.hostname_records, var.dns_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "service" {
|
||||||
|
for_each = local.all_records
|
||||||
|
|
||||||
|
zone_id = var.zone_id
|
||||||
|
name = each.value.name
|
||||||
|
content = each.value.value
|
||||||
|
type = each.value.type
|
||||||
|
proxied = each.value.proxied
|
||||||
|
ttl = each.value.ttl
|
||||||
|
}
|
||||||
9
modules/10-services-generic/cloudflare-dns/outputs.tf
Normal file
9
modules/10-services-generic/cloudflare-dns/outputs.tf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
output "dns_records" {
|
||||||
|
description = "Map of DNS records created"
|
||||||
|
value = cloudflare_record.service
|
||||||
|
}
|
||||||
|
|
||||||
|
output "record_hostnames" {
|
||||||
|
description = "List of hostnames for which DNS records were created"
|
||||||
|
value = keys(local.all_records)
|
||||||
|
}
|
||||||
46
modules/10-services-generic/cloudflare-dns/variables.tf
Normal file
46
modules/10-services-generic/cloudflare-dns/variables.tf
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
variable "zone_id" {
|
||||||
|
description = "Cloudflare Zone ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "dns_records" {
|
||||||
|
description = "Map of DNS records to create"
|
||||||
|
type = map(object({
|
||||||
|
name = string
|
||||||
|
value = string
|
||||||
|
type = string
|
||||||
|
proxied = bool
|
||||||
|
ttl = number
|
||||||
|
}))
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hostnames" {
|
||||||
|
description = "List of hostnames to create DNS records for"
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "target_content" {
|
||||||
|
description = "Target content/value for the DNS records when using hostnames list"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "record_type" {
|
||||||
|
description = "Record type for the DNS records when using hostnames list"
|
||||||
|
type = string
|
||||||
|
default = "CNAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "proxied" {
|
||||||
|
description = "Whether the records should be proxied through Cloudflare"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ttl" {
|
||||||
|
description = "TTL for the records (only used when proxied=false)"
|
||||||
|
type = number
|
||||||
|
default = 1 # Auto
|
||||||
|
}
|
||||||
@@ -139,15 +139,36 @@ resource "docker_container" "service_container" {
|
|||||||
cpu_shares = var.cpu_shares
|
cpu_shares = var.cpu_shares
|
||||||
|
|
||||||
# Other container options
|
# Other container options
|
||||||
dns = var.dns
|
dns = var.dns
|
||||||
dns_search = var.dns_search
|
dns_search = var.dns_search
|
||||||
hostname = var.hostname
|
hostname = var.hostname
|
||||||
domainname = var.domainname
|
domainname = var.domainname
|
||||||
user = var.user
|
user = var.user
|
||||||
working_dir = var.working_dir
|
group_add = var.group_add
|
||||||
command = var.command
|
working_dir = var.working_dir
|
||||||
entrypoint = var.entrypoint
|
command = var.command
|
||||||
privileged = var.privileged
|
entrypoint = var.entrypoint
|
||||||
|
privileged = var.privileged
|
||||||
|
destroy_grace_seconds = var.destroy_grace_seconds
|
||||||
|
|
||||||
|
# Linux capabilities controls
|
||||||
|
dynamic "capabilities" {
|
||||||
|
for_each = length(var.capabilities_add) > 0 || length(var.capabilities_drop) > 0 ? [1] : []
|
||||||
|
content {
|
||||||
|
add = var.capabilities_add
|
||||||
|
drop = var.capabilities_drop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Device mappings
|
||||||
|
dynamic "devices" {
|
||||||
|
for_each = var.devices
|
||||||
|
content {
|
||||||
|
host_path = devices.value.host_path
|
||||||
|
container_path = devices.value.container_path
|
||||||
|
permissions = devices.value.permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Set log options
|
# Set log options
|
||||||
log_driver = var.log_driver
|
log_driver = var.log_driver
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -195,8 +231,8 @@ variable "log_driver" {
|
|||||||
variable "log_opts" {
|
variable "log_opts" {
|
||||||
description = "Log driver options"
|
description = "Log driver options"
|
||||||
type = map(string)
|
type = map(string)
|
||||||
default = {
|
default = {
|
||||||
max-size = "10m"
|
max-size = "10m"
|
||||||
max-file = "3"
|
max-file = "3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
EMULATORJS_FRONTEND_PORT=5823
|
|
||||||
EMULATORJS_CONFIG_PORT=5824
|
|
||||||
EMULATORJS_BACKEND_PORT=5825
|
|
||||||
@@ -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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
77
modules/20-services-apps/glance/README.md
Normal file
77
modules/20-services-apps/glance/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Glance Module
|
||||||
|
|
||||||
|
This module deploys [Glance](https://glanceapp.io/), a dashboard application, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Glance module:
|
||||||
|
|
||||||
|
- Deploys the `glanceapp/glance` Docker container
|
||||||
|
- Persists configuration to a volume on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "glance" {
|
||||||
|
source = "./modules/20-services-apps/glance"
|
||||||
|
volume_path = "/path/to/volumes/glance"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the Glance image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for Glance data volume | `string` | - |
|
||||||
|
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| 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 = "glance"
|
||||||
|
primary_port = 4921
|
||||||
|
endpoint = "http://glance:4921"
|
||||||
|
subdomains = ["glance"]
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## 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 "glance" {
|
||||||
|
source = "./modules/20-services-apps/glance"
|
||||||
|
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.glance.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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,9 @@ 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
modules/20-services-apps/immich/.env.example
Normal file
14
modules/20-services-apps/immich/.env.example
Normal 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
|
||||||
110
modules/20-services-apps/immich/README.md
Normal file
110
modules/20-services-apps/immich/README.md
Normal 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
|
||||||
|
]
|
||||||
|
}
|
||||||
197
modules/20-services-apps/immich/main.tf
Normal file
197
modules/20-services-apps/immich/main.tf
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
4
modules/20-services-apps/jellyfin/.env.example
Normal file
4
modules/20-services-apps/jellyfin/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Optional values for Jellyfin module
|
||||||
|
# Only needed if you enable JELLYFIN_PublishedServerUrl in main.tf
|
||||||
|
|
||||||
|
HOSTNAME=example.com
|
||||||
87
modules/20-services-apps/jellyfin/README.md
Normal file
87
modules/20-services-apps/jellyfin/README.md
Normal 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`.
|
||||||
96
modules/20-services-apps/jellyfin/main.tf
Normal file
96
modules/20-services-apps/jellyfin/main.tf
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
90
modules/20-services-apps/linkwarden/README.md
Normal file
90
modules/20-services-apps/linkwarden/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Linkwarden Module
|
||||||
|
|
||||||
|
This module deploys [Linkwarden](https://linkwarden.app/), a self-hosted bookmark manager and link archive, as Docker containers in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Linkwarden module:
|
||||||
|
|
||||||
|
- Deploys two Docker containers:
|
||||||
|
- `linkwarden`: The main application server (Next.js)
|
||||||
|
- `postgres`: A PostgreSQL database backend
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "linkwarden" {
|
||||||
|
source = "./modules/20-services-apps/linkwarden"
|
||||||
|
volume_path = "/path/to/volumes/linkwarden"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the Linkwarden image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for Linkwarden and Postgres data volumes | `string` | - |
|
||||||
|
| `networks` | List of networks to which containers 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 = "linkwarden"
|
||||||
|
primary_port = 3000
|
||||||
|
endpoint = "http://linkwarden:3000"
|
||||||
|
subdomains = ["links"]
|
||||||
|
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Linkwarden 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:
|
||||||
|
|
||||||
|
- `NEXTAUTH_SECRET`: A secret key for NextAuth.js
|
||||||
|
- `NEXTAUTH_URL`: The public URL where Linkwarden will be accessed
|
||||||
|
- `POSTGRES_PASSWORD`: Password for the PostgreSQL database
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
Linkwarden stores its data in two volumes:
|
||||||
|
|
||||||
|
1. Linkwarden data: `/data/data` in the container, mapped to `${volume_path}/data` on the host
|
||||||
|
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/pgdata` 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 "linkwarden" {
|
||||||
|
source = "./modules/20-services-apps/linkwarden"
|
||||||
|
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.linkwarden.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -90,5 +90,6 @@ output "service_definition" {
|
|||||||
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 = ["links"]
|
subdomains = ["links"]
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=*
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e;
|
|
||||||
|
|
||||||
|
|
||||||
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
|
|
||||||
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
|
|
||||||
EOSQL
|
|
||||||
else
|
|
||||||
echo "SETUP INFO: No Environment variables given!"
|
|
||||||
fi
|
|
||||||
@@ -1,133 +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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
78
modules/20-services-apps/portainer/README.md
Normal file
78
modules/20-services-apps/portainer/README.md
Normal 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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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,14 +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"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Pterodactyl Panel Environment Settings
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
MYSQL_PASSWORD=secure_database_password_here
|
|
||||||
MYSQL_ROOT_PASSWORD=secure_root_password_here
|
|
||||||
MYSQL_DATABASE=pterodactyl
|
|
||||||
MYSQL_USER=pterodactyl
|
|
||||||
|
|
||||||
# Panel Configuration
|
|
||||||
APP_URL=https://panel.yourdomain.com
|
|
||||||
APP_TIMEZONE=Australia/Brisbane
|
|
||||||
APP_SERVICE_AUTHOR=email@example.com
|
|
||||||
APP_CORS_ALLOWED_ORIGINS=https://panel.yourdomain.com
|
|
||||||
TRUSTED_PROXIES="*" # Set this to your proxy IP
|
|
||||||
|
|
||||||
# Optional: Let's Encrypt Settings
|
|
||||||
# Uncomment and set to your email to use Let's Encrypt
|
|
||||||
# LE_EMAIL=admin@yourdomain.com
|
|
||||||
@@ -1,163 +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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +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/lib/pterodactyl/"
|
|
||||||
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 = "/tmp/pterodactyl/"
|
|
||||||
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.32.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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
72
services/main.tf
Normal file → Executable file
72
services/main.tf
Normal file → Executable 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,15 +20,41 @@ 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"
|
||||||
|
|
||||||
|
name = "media-network"
|
||||||
|
driver = "bridge"
|
||||||
|
attachable = true
|
||||||
|
subnet = "10.110.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "calibre" {
|
||||||
|
source = "${local.module_dir}/20-services-apps/calibre"
|
||||||
|
volume_path = "${local.volume_host}/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" {
|
||||||
@@ -35,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
12
services/outputs.tf
Normal file → Executable file
@@ -4,14 +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.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
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user