Compare commits
14 Commits
coolify
...
media-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07f59d021 | ||
|
|
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
@@ -64,23 +64,6 @@ provider "registry.opentofu.org/hashicorp/local" {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
provider "registry.opentofu.org/hashicorp/null" {
|
|
||||||
version = "3.2.4"
|
|
||||||
hashes = [
|
|
||||||
"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=",
|
|
||||||
"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3",
|
|
||||||
"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb",
|
|
||||||
"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2",
|
|
||||||
"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4",
|
|
||||||
"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d",
|
|
||||||
"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6",
|
|
||||||
"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072",
|
|
||||||
"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447",
|
|
||||||
"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58",
|
|
||||||
"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
22
main.tf
22
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"
|
||||||
}
|
}
|
||||||
@@ -22,3 +30,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 = module.system_globals.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 ntfy (reverse-proxy only with direct IP exposure)
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for a notification service"
|
||||||
|
value = {
|
||||||
|
name = "ntfy"
|
||||||
|
primary_port = 80
|
||||||
|
endpoint = "http://ntfy:80"
|
||||||
|
subdomains = ["ntfy"]
|
||||||
|
publish_via = "reverse_proxy" # Only expose via Caddy reverse proxy
|
||||||
|
proxied = false # Don't proxy through Cloudflare (expose direct IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
131
modules/01-networking/caddy-proxy/main.tf
Normal file
131
modules/01-networking/caddy-proxy/main.tf
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "docker_volume" "caddy_config" {
|
||||||
|
name = "${local.container_name}_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = "9080"
|
||||||
|
internal = "80"
|
||||||
|
protocol = "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
external = "9443"
|
||||||
|
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 = ""
|
||||||
|
}
|
||||||
@@ -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 = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
docker = {
|
|
||||||
source = "kreuzwerker/docker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate Caddyfile content
|
|
||||||
locals {
|
|
||||||
# Generate the Caddyfile header
|
|
||||||
caddyfile_header = "# Global options\n{\n admin :2019\n}\n\n"
|
|
||||||
|
|
||||||
# Generate site configurations - using separate HTTP and HTTPS blocks but with different hostnames
|
|
||||||
site_blocks = flatten([
|
|
||||||
for site in var.sites : [
|
|
||||||
# HTTPS configuration
|
|
||||||
"# Site configuration for ${site.domain} (HTTPS)",
|
|
||||||
"${site.domain}:${var.https_port} {",
|
|
||||||
" # TLS configuration",
|
|
||||||
" tls internal",
|
|
||||||
"",
|
|
||||||
" # Route configurations",
|
|
||||||
join("\n\n ", [
|
|
||||||
for route in site.routes : <<-ROUTE
|
|
||||||
# Route: ${route.path}
|
|
||||||
handle ${route.path} {
|
|
||||||
reverse_proxy ${route.target_host}:${route.target_port} {
|
|
||||||
${route.websocket ? "# WebSocket protocol handling\n header_up Connection \"Upgrade\"\n header_up Upgrade \"websocket\"" : ""}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ROUTE
|
|
||||||
]),
|
|
||||||
"}",
|
|
||||||
"",
|
|
||||||
# HTTP configuration with redirect to HTTPS
|
|
||||||
"# HTTP redirect for ${site.domain}",
|
|
||||||
":${var.http_port} {",
|
|
||||||
" redir https://${site.domain}:${var.https_port}{uri} permanent",
|
|
||||||
"}"
|
|
||||||
]
|
|
||||||
])
|
|
||||||
|
|
||||||
# Combine everything into the final Caddyfile
|
|
||||||
caddyfile_content = "${local.caddyfile_header}${join("\n\n", local.site_blocks)}"
|
|
||||||
|
|
||||||
# Define volumes for Caddy
|
|
||||||
volumes = [
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/Caddyfile"
|
|
||||||
container_path = "/etc/caddy/Caddyfile"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/data"
|
|
||||||
container_path = "/data"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/config"
|
|
||||||
container_path = "/config"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create data directories if they don't exist
|
|
||||||
data_directories = [
|
|
||||||
var.volume_path,
|
|
||||||
"${var.volume_path}/data",
|
|
||||||
"${var.volume_path}/config"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Environment variables - convert to the format docker_container resource expects
|
|
||||||
env_vars = var.networks != [] ? ["CADDY_INGRESS_NETWORKS=${join(",", var.networks)}"] : []
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
healthcheck = {
|
|
||||||
test = ["CMD", "wget", "--spider", "--quiet", "http://localhost:80/healthz"]
|
|
||||||
interval = "30s"
|
|
||||||
timeout = "3s"
|
|
||||||
retries = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create data directories
|
|
||||||
resource "null_resource" "create_directories" {
|
|
||||||
count = length(local.data_directories)
|
|
||||||
|
|
||||||
provisioner "local-exec" {
|
|
||||||
command = "mkdir -p ${local.data_directories[count.index]}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create Caddyfile
|
|
||||||
resource "local_file" "caddyfile" {
|
|
||||||
content = local.caddyfile_content
|
|
||||||
filename = "${var.volume_path}/Caddyfile"
|
|
||||||
depends_on = [null_resource.create_directories]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pull the image
|
|
||||||
resource "docker_image" "caddy" {
|
|
||||||
name = "${var.image}:${var.tag}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create the container
|
|
||||||
resource "docker_container" "caddy" {
|
|
||||||
name = var.container_name
|
|
||||||
image = docker_image.caddy.image_id
|
|
||||||
|
|
||||||
restart = "unless-stopped"
|
|
||||||
|
|
||||||
# Map ports similar to Nginx Proxy Manager
|
|
||||||
ports {
|
|
||||||
internal = 80
|
|
||||||
external = var.http_port
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
ports {
|
|
||||||
internal = 443
|
|
||||||
external = var.https_port
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Admin interface port
|
|
||||||
ports {
|
|
||||||
internal = 2019 # Caddy admin API port
|
|
||||||
external = var.admin_port
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set up volumes
|
|
||||||
dynamic "volumes" {
|
|
||||||
for_each = local.volumes
|
|
||||||
content {
|
|
||||||
host_path = volumes.value.host_path
|
|
||||||
container_path = volumes.value.container_path
|
|
||||||
read_only = volumes.value.read_only
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set environment variables as a list of strings in KEY=VALUE format
|
|
||||||
env = local.env_vars
|
|
||||||
|
|
||||||
# Set networks
|
|
||||||
dynamic "networks_advanced" {
|
|
||||||
for_each = var.networks
|
|
||||||
content {
|
|
||||||
name = networks_advanced.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set health check
|
|
||||||
healthcheck {
|
|
||||||
test = local.healthcheck.test
|
|
||||||
interval = local.healthcheck.interval
|
|
||||||
timeout = local.healthcheck.timeout
|
|
||||||
retries = local.healthcheck.retries
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add watchtower label if monitoring is enabled
|
|
||||||
labels {
|
|
||||||
label = "com.centurylinklabs.watchtower.enable"
|
|
||||||
value = var.monitoring ? "true" : "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make sure Caddyfile is created before starting container
|
|
||||||
depends_on = [local_file.caddyfile]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
output "container_name" {
|
|
||||||
description = "Name of the Caddy proxy container"
|
|
||||||
value = docker_container.caddy.name
|
|
||||||
}
|
|
||||||
|
|
||||||
output "container_ip" {
|
|
||||||
description = "IP address of the Caddy container"
|
|
||||||
value = docker_container.caddy.network_data[0].ip_address
|
|
||||||
}
|
|
||||||
|
|
||||||
output "domains" {
|
|
||||||
description = "Domains being proxied by Caddy"
|
|
||||||
value = [for site in var.sites : site.domain]
|
|
||||||
}
|
|
||||||
|
|
||||||
output "ready" {
|
|
||||||
description = "Boolean indicating if Caddy is ready"
|
|
||||||
value = docker_container.caddy.id != ""
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
variable "container_name" {
|
|
||||||
description = "The name of the Caddy container"
|
|
||||||
type = string
|
|
||||||
default = "caddy-proxy"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "http_port" {
|
|
||||||
description = "External HTTP port mapping"
|
|
||||||
type = number
|
|
||||||
default = 9080
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "https_port" {
|
|
||||||
description = "External HTTPS port mapping"
|
|
||||||
type = number
|
|
||||||
default = 9443
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "admin_port" {
|
|
||||||
description = "External admin port mapping"
|
|
||||||
type = number
|
|
||||||
default = 9081
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "image" {
|
|
||||||
description = "The image to use for the Caddy container"
|
|
||||||
type = string
|
|
||||||
default = "caddy"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "tag" {
|
|
||||||
description = "The tag of the Caddy image"
|
|
||||||
type = string
|
|
||||||
default = "latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "volume_path" {
|
|
||||||
description = "Base directory for volumes"
|
|
||||||
type = string
|
|
||||||
default = "/mnt/appdata/caddy"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "monitoring" {
|
|
||||||
description = "Enable or disable monitoring"
|
|
||||||
type = bool
|
|
||||||
default = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "networks" {
|
|
||||||
description = "List of networks to attach to the Caddy container"
|
|
||||||
type = list(string)
|
|
||||||
default = []
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "sites" {
|
|
||||||
description = "List of sites to proxy"
|
|
||||||
type = list(object({
|
|
||||||
domain = string # Domain name (e.g. deploy.yuris.dev)
|
|
||||||
routes = list(object({
|
|
||||||
path = string # Path to match (e.g. "/app/*" or "/" for root)
|
|
||||||
target_host = string # Target host (e.g. coolify)
|
|
||||||
target_port = number # Target port (e.g. 8080)
|
|
||||||
websocket = bool # Whether this route should be treated as websocket
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
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
|
||||||
|
}
|
||||||
77
modules/20-services-apps/actualbudget/README.md
Normal file
77
modules/20-services-apps/actualbudget/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# ActualBudget Module
|
||||||
|
|
||||||
|
This module deploys [ActualBudget](https://actualbudget.com/), a personal finance and budgeting application, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The ActualBudget module:
|
||||||
|
|
||||||
|
- Deploys the `actualbudget/actual-server` Docker container
|
||||||
|
- Persists data to a volume on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "actualbudget" {
|
||||||
|
source = "./modules/20-services-apps/actualbudget"
|
||||||
|
volume_path = "/path/to/volumes/actualbudget"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the ActualBudget image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for ActualBudget data volume | `string` | - |
|
||||||
|
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "actualbudget"
|
||||||
|
primary_port = 5006
|
||||||
|
endpoint = "http://actualbudget:5006"
|
||||||
|
subdomains = ["budget"]
|
||||||
|
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
ActualBudget stores its data in the `/data` directory inside the container. This is mapped to a volume on the host at `${volume_path}/data`.
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "actualbudget" {
|
||||||
|
source = "./modules/20-services-apps/actualbudget"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.actualbudget.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -47,5 +47,6 @@ output "service_definition" {
|
|||||||
primary_port = local.exposed_port
|
primary_port = local.exposed_port
|
||||||
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
||||||
subdomains = local.subdomains
|
subdomains = local.subdomains
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
modules/20-services-apps/affine/.env.example
Normal file
17
modules/20-services-apps/affine/.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Affine Configuration
|
||||||
|
AFFINE_REVISION=canary
|
||||||
|
PORT=3010
|
||||||
|
|
||||||
|
AFFINE_SERVER_HTTPS=true
|
||||||
|
AFFINE_SERVER_HOST=affine.yourdomain.com
|
||||||
|
AFFINE_SERVER_NAME='AFFiNE Selfhosted'
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_USERNAME=affine
|
||||||
|
DB_PASSWORD=change_this_password
|
||||||
|
DB_DATABASE=affine
|
||||||
|
|
||||||
|
# R2 Configuration
|
||||||
|
R2_OBJECT_STORAGE_ACCOUNT_ID=
|
||||||
|
R2_OBJECT_STORAGE_ACCESS_KEY_ID=
|
||||||
|
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY=
|
||||||
121
modules/20-services-apps/affine/README.md
Normal file
121
modules/20-services-apps/affine/README.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# AFFiNE Module
|
||||||
|
|
||||||
|
This module deploys [AFFiNE](https://affine.pro/), a privacy-first, local-first, note-taking and knowledge base application, as Docker containers in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AFFiNE module:
|
||||||
|
|
||||||
|
- Deploys four Docker containers:
|
||||||
|
- `affine_server`: The main AFFiNE application server
|
||||||
|
- `affine_migration_job`: A container that runs pre-deployment migrations
|
||||||
|
- `affine_postgres`: A PostgreSQL (pgvector) database backend
|
||||||
|
- `affine_redis`: A Redis instance for caching and temporary data
|
||||||
|
- Creates a dedicated Docker network (`affine-network`) for container communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "affine" {
|
||||||
|
source = "./modules/20-services-apps/affine"
|
||||||
|
volume_path = "/path/to/volumes/affine"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | -------------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the AFFiNE image to use | `string` | `"stable"` |
|
||||||
|
| `volume_path` | Host path for AFFiNE and database data volumes | `string` | - |
|
||||||
|
| `networks` | List of additional networks to which AFFiNE should be attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "affine_server"
|
||||||
|
primary_port = 3010
|
||||||
|
endpoint = "http://affine_server:3010"
|
||||||
|
subdomains = ["affine"]
|
||||||
|
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
AFFiNE requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
|
||||||
|
|
||||||
|
- Database configuration:
|
||||||
|
|
||||||
|
- `DB_USERNAME`: PostgreSQL user
|
||||||
|
- `DB_PASSWORD`: PostgreSQL password
|
||||||
|
- `DB_DATABASE`: Database name (defaults to "affine")
|
||||||
|
|
||||||
|
- AFFiNE configuration:
|
||||||
|
|
||||||
|
- `AFFINE_REVISION`: Version of AFFiNE to use ("stable" or "canary") (defaults to "canary")
|
||||||
|
- `PORT`: External port for the AFFiNE server (defaults to 3010)
|
||||||
|
- `AFFINE_SERVER_HTTPS`: Whether to use HTTPS (defaults to "true")
|
||||||
|
- `AFFINE_SERVER_HOST`: Hostname for the AFFiNE server
|
||||||
|
- `AFFINE_SERVER_NAME`: Name for the AFFiNE server (defaults to "AFFiNE Selfhosted")
|
||||||
|
|
||||||
|
- Cloudflare R2 configuration:
|
||||||
|
- `R2_OBJECT_STORAGE_ACCOUNT_ID`: Cloudflare R2 account ID
|
||||||
|
- `R2_OBJECT_STORAGE_ACCESS_KEY_ID`: Cloudflare R2 access key ID
|
||||||
|
- `R2_OBJECT_STORAGE_SECRET_ACCESS_KEY`: Cloudflare R2 secret access key
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
AFFiNE stores its data in three main volumes:
|
||||||
|
|
||||||
|
1. AFFiNE application data: `/root/.affine/storage` in the container, mapped to `${volume_path}/self-host/storage` on the host
|
||||||
|
2. AFFiNE configuration: `/root/.affine/config` in the container, mapped to `${volume_path}/self-host/config` on the host
|
||||||
|
3. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/self-host/postgres/pgdata` on the host
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The module creates a dedicated Docker network named `affine-network` for communication between the AFFiNE components. The AFFiNE server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
The AFFiNE containers have the following dependencies:
|
||||||
|
|
||||||
|
- The main `affine_server` depends on PostgreSQL, Redis, and the migration job
|
||||||
|
- The migration job depends on PostgreSQL and Redis
|
||||||
|
- Both PostgreSQL and Redis use healthchecks to ensure they're ready before dependent services start
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "affine" {
|
||||||
|
source = "./modules/20-services-apps/affine"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.affine.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
198
modules/20-services-apps/affine/main.tf
Normal file
198
modules/20-services-apps/affine/main.tf
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
dotenv = {
|
||||||
|
source = "germanbrew/dotenv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image_tag" {
|
||||||
|
description = "The tag for the affine container image"
|
||||||
|
type = string
|
||||||
|
default = "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
module "smtp" {
|
||||||
|
source = "../../00-globals/smtp"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "affine-server"
|
||||||
|
migration_name = "affine-migration-job"
|
||||||
|
redis_name = "affine-redis"
|
||||||
|
postgres_name = "affine-postgres"
|
||||||
|
affine_image = "ghcr.io/yurisasc/affine-graphql"
|
||||||
|
postgres_image = "pgvector/pgvector"
|
||||||
|
redis_image = "redis"
|
||||||
|
affine_tag = provider::dotenv::get_by_key("AFFINE_REVISION", local.env_file)
|
||||||
|
postgres_tag = "pg16"
|
||||||
|
redis_tag = "latest"
|
||||||
|
monitoring = true
|
||||||
|
env_file = "${path.module}/.env"
|
||||||
|
affine_internal_port = 3010
|
||||||
|
|
||||||
|
# Define volumes
|
||||||
|
affine_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/self-host/storage"
|
||||||
|
container_path = "/root/.affine/storage"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/self-host/config"
|
||||||
|
container_path = "/root/.affine/config"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
migration_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/self-host/storage"
|
||||||
|
container_path = "/root/.affine/storage"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/self-host/config"
|
||||||
|
container_path = "/root/.affine/config"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
postgres_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/self-host/postgres/pgdata"
|
||||||
|
container_path = "/var/lib/postgresql/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Environment variables for postgres
|
||||||
|
postgres_env_vars = {
|
||||||
|
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||||
|
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||||
|
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
|
||||||
|
POSTGRES_INITDB_ARGS = "--data-checksums"
|
||||||
|
POSTGRES_HOST_AUTH_METHOD = "trust"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Environment variables for AFFiNE
|
||||||
|
affine_env_vars = {
|
||||||
|
REDIS_SERVER_HOST = local.redis_name
|
||||||
|
DATABASE_URL = "postgresql://${provider::dotenv::get_by_key("DB_USERNAME", local.env_file)}:${provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)}@${local.postgres_name}:5432/${provider::dotenv::get_by_key("DB_DATABASE", local.env_file)}"
|
||||||
|
AFFINE_INDEXER_ENABLED = "false"
|
||||||
|
AFFINE_SERVER_HTTPS = provider::dotenv::get_by_key("AFFINE_SERVER_HTTPS", local.env_file)
|
||||||
|
AFFINE_SERVER_HOST = provider::dotenv::get_by_key("AFFINE_SERVER_HOST", local.env_file)
|
||||||
|
AFFINE_SERVER_NAME = provider::dotenv::get_by_key("AFFINE_SERVER_NAME", local.env_file)
|
||||||
|
PORT = provider::dotenv::get_by_key("PORT", local.env_file)
|
||||||
|
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
||||||
|
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
||||||
|
DB_DATABASE = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
|
||||||
|
MAILER_HOST = module.smtp.mail_host
|
||||||
|
MAILER_PORT = module.smtp.mail_port
|
||||||
|
MAILER_USER = module.smtp.mail_username
|
||||||
|
MAILER_PASSWORD = module.smtp.mail_password
|
||||||
|
R2_OBJECT_STORAGE_ACCOUNT_ID = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_ACCOUNT_ID", local.env_file)
|
||||||
|
R2_OBJECT_STORAGE_ACCESS_KEY_ID = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_ACCESS_KEY_ID", local.env_file)
|
||||||
|
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY = provider::dotenv::get_by_key("R2_OBJECT_STORAGE_SECRET_ACCESS_KEY", local.env_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Healthcheck configuration for Redis
|
||||||
|
redis_healthcheck = {
|
||||||
|
test = ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval = "10s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 5
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Healthcheck configuration for Postgres
|
||||||
|
postgres_healthcheck = {
|
||||||
|
test = ["CMD", "pg_isready", "-U", provider::dotenv::get_by_key("DB_USERNAME", local.env_file), "-d", provider::dotenv::get_by_key("DB_DATABASE", local.env_file)]
|
||||||
|
interval = "10s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 5
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "affine_network" {
|
||||||
|
source = "../../01-networking/docker-network"
|
||||||
|
name = "affine-network"
|
||||||
|
subnet = "11.100.0.0/16"
|
||||||
|
driver = "bridge"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the Redis container
|
||||||
|
module "redis" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
container_name = local.redis_name
|
||||||
|
image = local.redis_image
|
||||||
|
tag = local.redis_tag
|
||||||
|
networks = [module.affine_network.name]
|
||||||
|
monitoring = local.monitoring
|
||||||
|
healthcheck = local.redis_healthcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the PostgreSQL container
|
||||||
|
module "postgres" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
container_name = local.postgres_name
|
||||||
|
image = local.postgres_image
|
||||||
|
tag = local.postgres_tag
|
||||||
|
volumes = local.postgres_volumes
|
||||||
|
env_vars = local.postgres_env_vars
|
||||||
|
networks = [module.affine_network.name]
|
||||||
|
monitoring = local.monitoring
|
||||||
|
healthcheck = local.postgres_healthcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the migration job container
|
||||||
|
module "migration" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
container_name = local.migration_name
|
||||||
|
image = local.affine_image
|
||||||
|
tag = local.affine_tag
|
||||||
|
volumes = local.migration_volumes
|
||||||
|
env_vars = local.affine_env_vars
|
||||||
|
command = ["sh", "-c", "node ./scripts/self-host-predeploy.js"]
|
||||||
|
networks = [module.affine_network.name]
|
||||||
|
monitoring = local.monitoring
|
||||||
|
depends_on = [module.postgres, module.redis]
|
||||||
|
restart_policy = "no"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the affine container
|
||||||
|
module "affine" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.affine_image
|
||||||
|
tag = local.affine_tag
|
||||||
|
volumes = local.affine_volumes
|
||||||
|
env_vars = local.affine_env_vars
|
||||||
|
networks = concat([module.affine_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
depends_on = [module.postgres, module.redis, module.migration]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "General service definition with optional ingress configuration"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = local.affine_internal_port
|
||||||
|
endpoint = "http://${local.container_name}:${local.affine_internal_port}"
|
||||||
|
subdomains = ["notes"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
102
modules/20-services-apps/calibre/README.md
Normal file
102
modules/20-services-apps/calibre/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Calibre Module
|
||||||
|
|
||||||
|
This module deploys [Calibre Web Automated](https://hub.docker.com/r/crocodilestick/calibre-web-automated), a web app for browsing, reading, and managing eBooks, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Calibre module:
|
||||||
|
|
||||||
|
- Deploys a Docker container:
|
||||||
|
- `calibre-web-automated`: The main Calibre Web application with automation features
|
||||||
|
- Creates a dedicated Docker network (`calibre-network`) for container communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "calibre" {
|
||||||
|
source = "./modules/20-services-apps/calibre"
|
||||||
|
volume_path = "/path/to/volumes"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
user_id = "1000"
|
||||||
|
group_id = "1000"
|
||||||
|
timezone = "UTC"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | --------------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the Calibre Web image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for Calibre data volumes | `string` | - |
|
||||||
|
| `networks` | List of additional networks to which Calibre should be attached | `list(string)` | `[]` |
|
||||||
|
| `user_id` | User ID for container permissions | `string` | `"1000"` |
|
||||||
|
| `group_id` | Group ID for container permissions | `string` | `"1000"` |
|
||||||
|
| `timezone` | Timezone for the container | `string` | `"UTC"` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "calibre-web-automated"
|
||||||
|
primary_port = 8083
|
||||||
|
endpoint = "http://calibre-web-automated:8083"
|
||||||
|
subdomains = ["calibre"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Mods
|
||||||
|
|
||||||
|
This module includes the Calibre Docker mod to add Calibre functionality to the container:
|
||||||
|
- `lscr.io/linuxserver/mods:universal-calibre-v7.16.0`
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
Calibre stores its data in three main volumes:
|
||||||
|
|
||||||
|
1. Configuration data: `/config` in the container, mapped to `${volume_path}/config` on the host
|
||||||
|
2. Book ingest directory: `/cwa-book-ingest` in the container, mapped to `${volume_path}/ingest` on the host
|
||||||
|
3. Calibre library: `/calibre-library` in the container, mapped to `${volume_path}/library` on the host
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The Calibre container is attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "calibre" {
|
||||||
|
source = "./modules/20-services-apps/calibre"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
user_id = module.system_globals.user_id
|
||||||
|
group_id = module.system_globals.group_id
|
||||||
|
timezone = module.system_globals.timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.calibre.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
104
modules/20-services-apps/calibre/main.tf
Normal file
104
modules/20-services-apps/calibre/main.tf
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
dotenv = {
|
||||||
|
source = "germanbrew/dotenv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image_tag" {
|
||||||
|
description = "The tag for the Calibre Web container image"
|
||||||
|
type = string
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
default = "1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
default = "1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
default = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "calibre-web-automated"
|
||||||
|
calibre_image = "crocodilestick/calibre-web-automated"
|
||||||
|
calibre_tag = var.image_tag
|
||||||
|
monitoring = true
|
||||||
|
env_file = "${path.module}/.env"
|
||||||
|
calibre_internal_port = 8083
|
||||||
|
docker_mods = "lscr.io/linuxserver/mods:universal-calibre-v7.16.0"
|
||||||
|
|
||||||
|
# Define volumes
|
||||||
|
calibre_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/config"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/ingest"
|
||||||
|
container_path = "/cwa-book-ingest"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/library"
|
||||||
|
container_path = "/calibre-library"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Environment variables for Calibre Web
|
||||||
|
calibre_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
DOCKER_MODS = local.docker_mods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the Calibre Web container
|
||||||
|
module "calibre" {
|
||||||
|
source = "../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.calibre_image
|
||||||
|
tag = local.calibre_tag
|
||||||
|
volumes = local.calibre_volumes
|
||||||
|
env_vars = local.calibre_env_vars
|
||||||
|
networks = concat(var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "General service definition with optional ingress configuration"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = local.calibre_internal_port
|
||||||
|
endpoint = "http://${local.container_name}:${local.calibre_internal_port}"
|
||||||
|
subdomains = ["calibre"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
REGISTRY_URL=ghcr.io
|
|
||||||
|
|
||||||
# App Configuration
|
|
||||||
APP_ID=
|
|
||||||
APP_NAME=Coolify
|
|
||||||
APP_KEY=
|
|
||||||
ROOT_USERNAME=
|
|
||||||
ROOT_USER_EMAIL=
|
|
||||||
ROOT_USER_PASSWORD=
|
|
||||||
PHP_MEMORY_LIMIT=256M
|
|
||||||
PHP_FPM_PM_CONTROL=dynamic
|
|
||||||
PHP_FPM_PM_START_SERVERS=1
|
|
||||||
PHP_FPM_PM_MIN_SPARE_SERVERS=1
|
|
||||||
PHP_FPM_PM_MAX_SPARE_SERVERS=10
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
DB_DATABASE=coolify
|
|
||||||
DB_USERNAME=coolify
|
|
||||||
DB_PASSWORD=GENERATE_RANDOM_STRING
|
|
||||||
|
|
||||||
# Redis Configuration
|
|
||||||
REDIS_PASSWORD=GENERATE_RANDOM_STRING
|
|
||||||
|
|
||||||
# Pusher/Soketi Configuration
|
|
||||||
APP_NAME=Coolify
|
|
||||||
SOKETI_DEBUG=false
|
|
||||||
PUSHER_APP_ID=GENERATE_RANDOM_HEXADECIMAL
|
|
||||||
PUSHER_APP_KEY=GENERATE_RANDOM_HEXADECIMAL
|
|
||||||
PUSHER_APP_SECRET=GENERATE_RANDOM_HEXADECIMAL
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
module "coolify_caddy" {
|
|
||||||
source = "../../10-services-generic/caddy-proxy"
|
|
||||||
container_name = "coolify-caddy"
|
|
||||||
volume_path = "${var.volume_path}/caddy"
|
|
||||||
networks = concat([module.coolify_network.name], var.networks)
|
|
||||||
|
|
||||||
# Use custom ports (not exposing 80/443 directly)
|
|
||||||
http_port = 7080
|
|
||||||
https_port = 6443
|
|
||||||
admin_port = 7081
|
|
||||||
|
|
||||||
sites = [
|
|
||||||
{
|
|
||||||
domain = "deploy.yuris.dev"
|
|
||||||
routes = [
|
|
||||||
{
|
|
||||||
path = "/app/*" # Main WebSocket endpoint for Pusher/Soketi
|
|
||||||
target_host = local.soketi_container_name
|
|
||||||
target_port = local.soketi_port
|
|
||||||
websocket = true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path = "/apps/*" # Alternative WebSocket endpoint
|
|
||||||
target_host = local.soketi_container_name
|
|
||||||
target_port = local.soketi_port
|
|
||||||
websocket = true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path = "/*" # All other requests go to the main app
|
|
||||||
target_host = local.app_container_name
|
|
||||||
target_port = local.app_port
|
|
||||||
websocket = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
dotenv = {
|
|
||||||
source = "germanbrew/dotenv"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "image_tag" {
|
|
||||||
description = "The tag for the coolify 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 coolify container should be attached"
|
|
||||||
type = list(string)
|
|
||||||
default = []
|
|
||||||
}
|
|
||||||
|
|
||||||
locals {
|
|
||||||
env_file = "${path.module}/.env"
|
|
||||||
|
|
||||||
# Container names
|
|
||||||
app_container_name = "coolify"
|
|
||||||
db_container_name = "coolify-db"
|
|
||||||
redis_container_name = "coolify-redis"
|
|
||||||
soketi_container_name = "coolify-realtime"
|
|
||||||
|
|
||||||
# Images and tags
|
|
||||||
app_image = "ghcr.io/coollabsio/coolify"
|
|
||||||
db_image = "postgres"
|
|
||||||
redis_image = "redis"
|
|
||||||
soketi_image = "ghcr.io/coollabsio/coolify-realtime"
|
|
||||||
|
|
||||||
app_tag = var.image_tag != "" ? var.image_tag : "latest"
|
|
||||||
db_tag = "15-alpine"
|
|
||||||
redis_tag = "7-alpine"
|
|
||||||
soketi_tag = "1.0.8"
|
|
||||||
|
|
||||||
monitoring = true
|
|
||||||
app_port = 8080
|
|
||||||
soketi_port = 6001
|
|
||||||
|
|
||||||
# Volume mappings
|
|
||||||
app_volumes = [
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/source/.env"
|
|
||||||
container_path = "/var/www/html/.env"
|
|
||||||
read_only = true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/ssh"
|
|
||||||
container_path = "/var/www/html/storage/app/ssh"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/applications"
|
|
||||||
container_path = "/var/www/html/storage/app/applications"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/databases"
|
|
||||||
container_path = "/var/www/html/storage/app/databases"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/services"
|
|
||||||
container_path = "/var/www/html/storage/app/services"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/backups"
|
|
||||||
container_path = "/var/www/html/storage/app/backups"
|
|
||||||
read_only = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/webhooks-during-maintenance"
|
|
||||||
container_path = "/var/www/html/storage/app/webhooks-during-maintenance"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
db_volumes = [
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/db_data"
|
|
||||||
container_path = "/var/lib/postgresql/data"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
redis_volumes = [
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/redis_data"
|
|
||||||
container_path = "/data"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
soketi_volumes = [
|
|
||||||
{
|
|
||||||
host_path = "${var.volume_path}/ssh"
|
|
||||||
container_path = "/var/www/html/storage/app/ssh"
|
|
||||||
read_only = false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
app_env_vars = {
|
|
||||||
APP_ENV = "production"
|
|
||||||
APP_ID = provider::dotenv::get_by_key("APP_ID", local.env_file)
|
|
||||||
APP_NAME = provider::dotenv::get_by_key("APP_NAME", local.env_file)
|
|
||||||
APP_KEY = provider::dotenv::get_by_key("APP_KEY", local.env_file)
|
|
||||||
ROOT_USERNAME = provider::dotenv::get_by_key("ROOT_USERNAME", local.env_file)
|
|
||||||
ROOT_USER_EMAIL = provider::dotenv::get_by_key("ROOT_USER_EMAIL", local.env_file)
|
|
||||||
ROOT_USER_PASSWORD = provider::dotenv::get_by_key("ROOT_USER_PASSWORD", local.env_file)
|
|
||||||
PHP_MEMORY_LIMIT = provider::dotenv::get_by_key("PHP_MEMORY_LIMIT", local.env_file)
|
|
||||||
PHP_FPM_PM_CONTROL = provider::dotenv::get_by_key("PHP_FPM_PM_CONTROL", local.env_file)
|
|
||||||
PHP_FPM_PM_START_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_START_SERVERS", local.env_file)
|
|
||||||
PHP_FPM_PM_MIN_SPARE_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_MIN_SPARE_SERVERS", local.env_file)
|
|
||||||
PHP_FPM_PM_MAX_SPARE_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_MAX_SPARE_SERVERS", local.env_file)
|
|
||||||
DB_DATABASE = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
|
|
||||||
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
|
||||||
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
|
||||||
REDIS_PASSWORD = provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
db_env_vars = {
|
|
||||||
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
|
|
||||||
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
|
|
||||||
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
redis_env_vars = {
|
|
||||||
REDIS_PASSWORD = provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
soketi_env_vars = {
|
|
||||||
APP_NAME = provider::dotenv::get_by_key("APP_NAME", local.env_file)
|
|
||||||
SOKETI_DEBUG = provider::dotenv::get_by_key("SOKETI_DEBUG", local.env_file)
|
|
||||||
SOKETI_DEFAULT_APP_ID = provider::dotenv::get_by_key("PUSHER_APP_ID", local.env_file)
|
|
||||||
SOKETI_DEFAULT_APP_KEY = provider::dotenv::get_by_key("PUSHER_APP_KEY", local.env_file)
|
|
||||||
SOKETI_DEFAULT_APP_SECRET = provider::dotenv::get_by_key("PUSHER_APP_SECRET", local.env_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
healthchecks = {
|
|
||||||
db = {
|
|
||||||
test = ["CMD-SHELL", "pg_isready -U coolify -d coolify"]
|
|
||||||
interval = "5s"
|
|
||||||
timeout = "2s"
|
|
||||||
retries = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
redis = {
|
|
||||||
test = ["CMD", "redis-cli", "ping"]
|
|
||||||
interval = "5s"
|
|
||||||
timeout = "2s"
|
|
||||||
retries = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
soketi = {
|
|
||||||
test = ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
|
|
||||||
interval = "5s"
|
|
||||||
timeout = "2s"
|
|
||||||
retries = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
app = {
|
|
||||||
test = ["CMD-SHELL", "curl --fail http://127.0.0.1:8080/api/health || exit 1"]
|
|
||||||
interval = "5s"
|
|
||||||
timeout = "2s"
|
|
||||||
retries = 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module "coolify_network" {
|
|
||||||
source = "../../01-networking/docker-network"
|
|
||||||
name = "coolify"
|
|
||||||
driver = "bridge"
|
|
||||||
}
|
|
||||||
|
|
||||||
module "postgres" {
|
|
||||||
source = "../../10-services-generic/docker-service"
|
|
||||||
container_name = local.db_container_name
|
|
||||||
image = local.db_image
|
|
||||||
tag = local.db_tag
|
|
||||||
volumes = local.db_volumes
|
|
||||||
puid = 9999
|
|
||||||
pgid = 9999
|
|
||||||
env_vars = local.db_env_vars
|
|
||||||
networks = [module.coolify_network.name]
|
|
||||||
monitoring = local.monitoring
|
|
||||||
healthcheck = local.healthchecks.db
|
|
||||||
}
|
|
||||||
|
|
||||||
module "redis" {
|
|
||||||
source = "../../10-services-generic/docker-service"
|
|
||||||
container_name = local.redis_container_name
|
|
||||||
image = local.redis_image
|
|
||||||
tag = local.redis_tag
|
|
||||||
volumes = local.redis_volumes
|
|
||||||
puid = 9999
|
|
||||||
pgid = 9999
|
|
||||||
env_vars = local.redis_env_vars
|
|
||||||
networks = [module.coolify_network.name]
|
|
||||||
monitoring = local.monitoring
|
|
||||||
command = ["redis-server", "--save", "20", "1", "--loglevel", "warning", "--requirepass", provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)]
|
|
||||||
healthcheck = local.healthchecks.redis
|
|
||||||
}
|
|
||||||
|
|
||||||
module "soketi" {
|
|
||||||
source = "../../10-services-generic/docker-service"
|
|
||||||
container_name = local.soketi_container_name
|
|
||||||
image = local.soketi_image
|
|
||||||
tag = local.soketi_tag
|
|
||||||
volumes = local.soketi_volumes
|
|
||||||
puid = 9999
|
|
||||||
pgid = 9999
|
|
||||||
env_vars = local.soketi_env_vars
|
|
||||||
networks = [module.coolify_network.name]
|
|
||||||
monitoring = local.monitoring
|
|
||||||
healthcheck = local.healthchecks.soketi
|
|
||||||
}
|
|
||||||
|
|
||||||
module "coolify" {
|
|
||||||
source = "../../10-services-generic/docker-service"
|
|
||||||
container_name = local.app_container_name
|
|
||||||
image = local.app_image
|
|
||||||
tag = local.app_tag
|
|
||||||
volumes = local.app_volumes
|
|
||||||
puid = 9999
|
|
||||||
pgid = 9999
|
|
||||||
env_vars = local.app_env_vars
|
|
||||||
networks = concat([module.coolify_network.name], var.networks)
|
|
||||||
monitoring = local.monitoring
|
|
||||||
healthcheck = local.healthchecks.app
|
|
||||||
host_mappings = [
|
|
||||||
{
|
|
||||||
host = "host.docker.internal"
|
|
||||||
ip = "host-gateway"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
depends_on = [module.postgres, module.redis, module.soketi]
|
|
||||||
}
|
|
||||||
|
|
||||||
output "service_definition" {
|
|
||||||
description = "Service definition with ingress configuration"
|
|
||||||
value = {
|
|
||||||
name = local.app_container_name
|
|
||||||
primary_port = local.app_port
|
|
||||||
endpoint = "http://${local.app_container_name}:${local.app_port}"
|
|
||||||
subdomains = ["deploy"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
99
modules/20-services-apps/emulatorjs/README.md
Normal file
99
modules/20-services-apps/emulatorjs/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# EmulatorJS Module
|
||||||
|
|
||||||
|
This module deploys [EmulatorJS](https://github.com/linuxserver/docker-emulatorjs), a self-hosted retro gaming emulation platform, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The EmulatorJS module:
|
||||||
|
|
||||||
|
- Deploys the `linuxserver/emulatorjs` Docker container
|
||||||
|
- Persists configuration and game data to volumes on the host
|
||||||
|
- Exposes multiple ports for frontend, configuration, and backend services
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "emulatorjs" {
|
||||||
|
source = "./modules/20-services-apps/emulatorjs"
|
||||||
|
volume_path = "/path/to/volumes/emulatorjs"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | -------------------------------------------- | -------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the EmulatorJS image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for EmulatorJS data volumes | `string` | - |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "emulatorjs"
|
||||||
|
primary_port = <frontend_port>
|
||||||
|
endpoint = "http://emulatorjs:<frontend_port>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that unlike other services, EmulatorJS doesn't specify subdomains or a publish method in its service definition. This may require manual configuration in your networking setup.
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
EmulatorJS exposes three ports, which are mapped to host ports defined in the `.env` file:
|
||||||
|
|
||||||
|
1. Frontend (port 80) - The main web interface for accessing games
|
||||||
|
2. Config (port 3000) - The configuration interface
|
||||||
|
3. Backend (port 4001) - Backend services
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
This module requires the following environment variables to be set in a `.env` file:
|
||||||
|
|
||||||
|
- `EMULATORJS_FRONTEND_PORT`: Host port for the main web interface
|
||||||
|
- `EMULATORJS_CONFIG_PORT`: Host port for the configuration interface
|
||||||
|
- `EMULATORJS_BACKEND_PORT`: Host port for backend services
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
EmulatorJS stores its data in two volumes:
|
||||||
|
|
||||||
|
1. Configuration: `/config` in the container, mapped to `${volume_path}/config` on the host
|
||||||
|
2. Game data: `/data` in the container, mapped to `${volume_path}/data` on the host
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "emulatorjs" {
|
||||||
|
source = "./modules/20-services-apps/emulatorjs"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
}
|
||||||
|
|
||||||
|
# If you want to expose EmulatorJS via your networking modules,
|
||||||
|
# you may need to manually configure the service definition:
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.emulatorjs.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Configuration
|
||||||
|
|
||||||
|
After deployment, you can access the configuration interface at `http://your-server:<config_port>` to:
|
||||||
|
|
||||||
|
1. Upload ROM files to the `/data/roms` directory
|
||||||
|
2. Configure emulation settings
|
||||||
|
3. Manage game art and metadata
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
180
modules/20-services-apps/media-server/README.md
Normal file
180
modules/20-services-apps/media-server/README.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Media Server Module
|
||||||
|
|
||||||
|
This module deploys a complete media server stack using Docker containers in the homelab environment. It includes content management services (Sonarr, Radarr, Readarr), content discovery (Prowlarr, Jellyseerr), download clients (Qbittorrent, Sabnzbd), and media playback (Jellyfin).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Media Server module:
|
||||||
|
|
||||||
|
- Deploys multiple Docker containers:
|
||||||
|
- `sonarr`: Series/TV management system
|
||||||
|
- `radarr`: Movie management system
|
||||||
|
- `readarr`: Book management system
|
||||||
|
- `jellyseerr`: Content request and discovery system
|
||||||
|
- `prowlarr`: Indexer management system
|
||||||
|
- `qbittorrent`: Torrent download client with VueTorrent UI
|
||||||
|
- `unpackerr`: Automatic extraction utility
|
||||||
|
- `jellyfin`: Media server for streaming content
|
||||||
|
- `sabnzbd`: Usenet download client
|
||||||
|
- `flaresolverr`: Proxy service to bypass Cloudflare and other protection
|
||||||
|
- `autoheal`: Service for automatic container health restarts
|
||||||
|
- Creates a dedicated Docker network (`media-server`) for container communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definitions for integration with networking modules
|
||||||
|
- Exposes Jellyfin, Jellyseerr, and Sabnzbd via reverse proxy
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "media_server" {
|
||||||
|
source = "./modules/20-services-apps/media-server"
|
||||||
|
volume_path = "/path/to/app/data"
|
||||||
|
data_root = "/path/to/media/data"
|
||||||
|
download_root = "/path/to/download/data"
|
||||||
|
user_id = "1000"
|
||||||
|
group_id = "1000"
|
||||||
|
timezone = "UTC"
|
||||||
|
hostname = "example.com"
|
||||||
|
sonarr_api_key = "your-sonarr-api-key"
|
||||||
|
radarr_api_key = "your-radarr-api-key"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ---------------- | ------------------------------------------------- | -------------- | -------- |
|
||||||
|
| `volume_path` | Base directory for application data | `string` | - |
|
||||||
|
| `data_root` | Root directory for media data | `string` | - |
|
||||||
|
| `download_root` | Directory for downloads | `string` | - |
|
||||||
|
| `user_id` | User ID for container permissions | `string` | `"1000"` |
|
||||||
|
| `group_id` | Group ID for container permissions | `string` | `"1000"` |
|
||||||
|
| `timezone` | Timezone for the containers | `string` | `"UTC"` |
|
||||||
|
| `hostname` | Hostname for the Jellyfin PublishedServerUrl | `string` | - |
|
||||||
|
| `sonarr_api_key` | API key for Sonarr | `string` | - |
|
||||||
|
| `radarr_api_key` | API key for Radarr | `string` | - |
|
||||||
|
| `networks` | List of networks to which containers are attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| --------------------- | ------------------------------------------------------------------ |
|
||||||
|
| `service_definitions` | Service definitions for Jellyfin, Jellyseerr, and Sabnzbd services |
|
||||||
|
| `network_name` | Name of the media server network |
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
### Sonarr
|
||||||
|
TV series management tool that integrates with various download clients and indexers to automate obtaining TV episodes.
|
||||||
|
- Port: 8989
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/sonarr`
|
||||||
|
- Data: `/data` → `${data_root}`
|
||||||
|
|
||||||
|
### Radarr
|
||||||
|
Movie management tool that integrates with various download clients and indexers to automate obtaining movies.
|
||||||
|
- Port: 7878
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/radarr`
|
||||||
|
- Data: `/data` → `${data_root}`
|
||||||
|
|
||||||
|
### Readarr
|
||||||
|
Book management tool that integrates with various download clients and indexers to automate obtaining books.
|
||||||
|
- Port: 8787
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/readarr`
|
||||||
|
- Books: `/books` → `${data_root}`
|
||||||
|
|
||||||
|
### Jellyseerr
|
||||||
|
Media request system that integrates with Jellyfin to allow users to request new content.
|
||||||
|
- Port: 5055
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/app/config` → `${volume_path}/jellyseerr`
|
||||||
|
|
||||||
|
### Prowlarr
|
||||||
|
Indexer manager/proxy that integrates with various PVR apps and download clients.
|
||||||
|
- Port: 9696
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/prowlarr`
|
||||||
|
|
||||||
|
### FlareSolverr
|
||||||
|
Proxy server to bypass Cloudflare and other protection methods.
|
||||||
|
- Port: 8191
|
||||||
|
|
||||||
|
### QBittorrent
|
||||||
|
Torrent client with VueTorrent web interface.
|
||||||
|
- Port: 8080
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/qbittorrent`
|
||||||
|
- Downloads: `/data/torrents` → `${download_root}`
|
||||||
|
|
||||||
|
### Unpackerr
|
||||||
|
Extracts completed downloads automatically for Sonarr, Radarr, and others.
|
||||||
|
- Volumes:
|
||||||
|
- Downloads: `/data/torrents` → `${download_root}`
|
||||||
|
|
||||||
|
### Jellyfin
|
||||||
|
Media server for streaming content to various devices.
|
||||||
|
- Ports:
|
||||||
|
- 8096 (HTTP)
|
||||||
|
- 7359 (UDP)
|
||||||
|
- 1900 (UDP)
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/jellyfin`
|
||||||
|
- Data: `/data` → `${data_root}`
|
||||||
|
- Devices:
|
||||||
|
- `/dev/dri/` for hardware acceleration
|
||||||
|
|
||||||
|
### Sabnzbd
|
||||||
|
Usenet download client.
|
||||||
|
- Port: 6789 (external) → 8080 (internal)
|
||||||
|
- Volumes:
|
||||||
|
- Config: `/config` → `${volume_path}/sabnzbd/config`
|
||||||
|
- Downloads: `/downloads` → `${data_root}/usenet/downloads`
|
||||||
|
|
||||||
|
### Autoheal
|
||||||
|
Service that automatically checks for container health and restarts unhealthy containers.
|
||||||
|
- Requires access to Docker socket
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The module creates a dedicated Docker network named `media-server` for communication between the components. Each service container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||||
|
|
||||||
|
## Reverse Proxy Integration
|
||||||
|
|
||||||
|
Three services are configured to be exposed through a reverse proxy:
|
||||||
|
|
||||||
|
- **Jellyfin**: Exposed at subdomain `jellyfin`
|
||||||
|
- **Jellyseerr**: Exposed at subdomain `requests`
|
||||||
|
- **Sabnzbd**: Exposed at subdomain `sabnzbd`
|
||||||
|
|
||||||
|
These services have their `publish_via` property set to `"reverse_proxy"` in their service definitions.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "media_server" {
|
||||||
|
source = "./modules/20-services-apps/media-server"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
data_root = "${module.system_globals.data_root}/media"
|
||||||
|
download_root = "${module.system_globals.data_root}/downloads"
|
||||||
|
user_id = module.system_globals.user_id
|
||||||
|
group_id = module.system_globals.group_id
|
||||||
|
timezone = module.system_globals.timezone
|
||||||
|
hostname = "example.com"
|
||||||
|
sonarr_api_key = var.sonarr_api_key
|
||||||
|
radarr_api_key = var.radarr_api_key
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definitions are automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = concat(
|
||||||
|
module.media_server.service_definitions,
|
||||||
|
# Other service definitions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
148
modules/20-services-apps/media-server/main.tf
Normal file
148
modules/20-services-apps/media-server/main.tf
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
dotenv = {
|
||||||
|
source = "germanbrew/dotenv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
monitoring = true
|
||||||
|
|
||||||
|
# Define common healthcheck settings
|
||||||
|
healthcheck_interval = "30s"
|
||||||
|
healthcheck_retries = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create dedicated network for media server components
|
||||||
|
module "media_server_network" {
|
||||||
|
source = "../../01-networking/docker-network"
|
||||||
|
name = "media-server"
|
||||||
|
subnet = "11.102.0.0/16"
|
||||||
|
driver = "bridge"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import service modules
|
||||||
|
module "sonarr" {
|
||||||
|
source = "./services/sonarr"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
data_root = var.data_root
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "radarr" {
|
||||||
|
source = "./services/radarr"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
data_root = var.data_root
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "readarr" {
|
||||||
|
source = "./services/readarr"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
data_root = var.data_root
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jellyseerr" {
|
||||||
|
source = "./services/jellyseerr"
|
||||||
|
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "prowlarr" {
|
||||||
|
source = "./services/prowlarr"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "qbittorrent" {
|
||||||
|
source = "./services/qbittorrent"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
download_root = var.download_root
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "unpackerr" {
|
||||||
|
source = "./services/unpackerr"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
download_root = var.download_root
|
||||||
|
sonarr_api_key = var.sonarr_api_key
|
||||||
|
radarr_api_key = var.radarr_api_key
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jellyfin" {
|
||||||
|
source = "./services/jellyfin"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
data_root = var.data_root
|
||||||
|
hostname = var.hostname
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "sabnzbd" {
|
||||||
|
source = "./services/sabnzbd"
|
||||||
|
|
||||||
|
user_id = var.user_id
|
||||||
|
group_id = var.group_id
|
||||||
|
timezone = var.timezone
|
||||||
|
volume_path = var.volume_path
|
||||||
|
data_root = var.data_root
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "flaresolverr" {
|
||||||
|
source = "./services/flaresolverr"
|
||||||
|
|
||||||
|
timezone = var.timezone
|
||||||
|
log_level = "info"
|
||||||
|
log_html = "false"
|
||||||
|
captcha_solver = "none"
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
|
|
||||||
|
module "autoheal" {
|
||||||
|
source = "./services/autoheal"
|
||||||
|
|
||||||
|
networks = concat([module.media_server_network.name], var.networks)
|
||||||
|
monitoring = local.monitoring
|
||||||
|
}
|
||||||
13
modules/20-services-apps/media-server/outputs.tf
Normal file
13
modules/20-services-apps/media-server/outputs.tf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
output "service_definitions" {
|
||||||
|
description = "Service definitions for integration with networking modules"
|
||||||
|
value = [
|
||||||
|
module.jellyfin.service_definition,
|
||||||
|
module.jellyseerr.service_definition,
|
||||||
|
module.sabnzbd.service_definition
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "network_name" {
|
||||||
|
description = "Name of the media server network"
|
||||||
|
value = module.media_server_network.name
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "autoheal"
|
||||||
|
image = "willfarrell/autoheal"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
autoheal_env_vars = {
|
||||||
|
AUTOHEAL_CONTAINER_LABEL = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
autoheal_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "/var/run/docker.sock"
|
||||||
|
container_path = "/var/run/docker.sock"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module "autoheal" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.autoheal_volumes
|
||||||
|
env_vars = local.autoheal_env_vars
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for autoheal"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
endpoint = "http://${local.container_name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "log_level" {
|
||||||
|
description = "Log level for flaresolverr"
|
||||||
|
type = string
|
||||||
|
default = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "log_html" {
|
||||||
|
description = "Whether to log HTML"
|
||||||
|
type = string
|
||||||
|
default = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "captcha_solver" {
|
||||||
|
description = "Type of CAPTCHA solver to use"
|
||||||
|
type = string
|
||||||
|
default = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "flaresolverr"
|
||||||
|
image = "ghcr.io/flaresolverr/flaresolverr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
flaresolverr_env_vars = {
|
||||||
|
LOG_LEVEL = var.log_level
|
||||||
|
LOG_HTML = var.log_html
|
||||||
|
CAPTCHA_SOLVER = var.captcha_solver
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "flaresolverr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
env_vars = local.flaresolverr_env_vars
|
||||||
|
ports = [{
|
||||||
|
internal = 8191
|
||||||
|
external = 8191
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for flaresolverr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8191
|
||||||
|
endpoint = "http://${local.container_name}:8191"
|
||||||
|
}
|
||||||
|
}
|
||||||
114
modules/20-services-apps/media-server/services/jellyfin/main.tf
Normal file
114
modules/20-services-apps/media-server/services/jellyfin/main.tf
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hostname" {
|
||||||
|
description = "Hostname for the Jellyfin PublishedServerUrl"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "jellyfin"
|
||||||
|
image = "jellyfin/jellyfin"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
internal_ports = [
|
||||||
|
{
|
||||||
|
internal = 8096
|
||||||
|
external = 8096
|
||||||
|
protocol = "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
internal = 7359
|
||||||
|
external = 7359
|
||||||
|
protocol = "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
internal = 1900
|
||||||
|
external = 1900
|
||||||
|
protocol = "udp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
jellyfin_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/jellyfin"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.data_root}"
|
||||||
|
container_path = "/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
jellyfin_devices = [
|
||||||
|
"/dev/dri/:/dev/dri/"
|
||||||
|
]
|
||||||
|
|
||||||
|
jellyfin_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
JELLYFIN_PublishedServerUrl = "${var.hostname}/jellyfin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jellyfin" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.jellyfin_volumes
|
||||||
|
env_vars = local.jellyfin_env_vars
|
||||||
|
ports = local.internal_ports
|
||||||
|
devices = local.jellyfin_devices
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for integration with networking modules"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8096
|
||||||
|
endpoint = "http://${local.container_name}:8096"
|
||||||
|
subdomains = ["jellyfin"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "jellyseerr"
|
||||||
|
image = "fallenbagel/jellyseerr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
jellyseerr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/jellyseerr"
|
||||||
|
container_path = "/app/config"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
jellyseerr_env_vars = {
|
||||||
|
LOG_LEVEL = "debug"
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
jellyseerr_healthcheck = {
|
||||||
|
test = ["CMD", "wget", "http://127.0.0.1:5055/api/v1/status", "-qO", "/dev/null"]
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 10
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jellyseerr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.jellyseerr_volumes
|
||||||
|
env_vars = local.jellyseerr_env_vars
|
||||||
|
healthcheck = local.jellyseerr_healthcheck
|
||||||
|
ports = [{
|
||||||
|
internal = 5055
|
||||||
|
external = 5055
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for integration with networking modules"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 5055
|
||||||
|
endpoint = "http://${local.container_name}:5055"
|
||||||
|
subdomains = ["requests"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "prowlarr"
|
||||||
|
image = "lscr.io/linuxserver/prowlarr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
prowlarr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/prowlarr"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
prowlarr_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
prowlarr_healthcheck = {
|
||||||
|
test = ["CMD", "curl", "--fail", "http://127.0.0.1:9696/prowlarr/ping"]
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 10
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "prowlarr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.prowlarr_volumes
|
||||||
|
env_vars = local.prowlarr_env_vars
|
||||||
|
healthcheck = local.prowlarr_healthcheck
|
||||||
|
ports = [{
|
||||||
|
internal = 9696
|
||||||
|
external = 9696
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for prowlarr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 9696
|
||||||
|
endpoint = "http://${local.container_name}:9696"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "download_root" {
|
||||||
|
description = "Directory for downloads"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "qbittorrent"
|
||||||
|
image = "lscr.io/linuxserver/qbittorrent"
|
||||||
|
tag = "libtorrentv1"
|
||||||
|
|
||||||
|
qbittorrent_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/qbittorrent"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = var.download_root
|
||||||
|
container_path = "/data/torrents"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
qbittorrent_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
WEBUI_PORT = "8080"
|
||||||
|
DOCKER_MODS = "ghcr.io/gabe565/linuxserver-mod-vuetorrent"
|
||||||
|
}
|
||||||
|
|
||||||
|
qbittorrent_healthcheck = {
|
||||||
|
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 10
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "qbittorrent" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.qbittorrent_volumes
|
||||||
|
env_vars = local.qbittorrent_env_vars
|
||||||
|
healthcheck = local.qbittorrent_healthcheck
|
||||||
|
ports = [{
|
||||||
|
internal = 8080
|
||||||
|
external = 8080
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for qbittorrent"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8080
|
||||||
|
endpoint = "http://${local.container_name}:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "radarr"
|
||||||
|
image = "lscr.io/linuxserver/radarr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
radarr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/radarr"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = var.data_root
|
||||||
|
container_path = "/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
radarr_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
radarr_healthcheck = {
|
||||||
|
test = ["CMD", "curl", "--fail", "http://127.0.0.1:7878/radarr/ping"]
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 10
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "radarr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.radarr_volumes
|
||||||
|
env_vars = local.radarr_env_vars
|
||||||
|
healthcheck = local.radarr_healthcheck
|
||||||
|
ports = [{
|
||||||
|
internal = 7878
|
||||||
|
external = 7878
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for radarr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 7878
|
||||||
|
endpoint = "http://${local.container_name}:7878"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "readarr"
|
||||||
|
image = "lscr.io/linuxserver/readarr"
|
||||||
|
tag = "develop"
|
||||||
|
|
||||||
|
readarr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/readarr"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = var.data_root
|
||||||
|
container_path = "/books"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
readarr_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "readarr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.readarr_volumes
|
||||||
|
env_vars = local.readarr_env_vars
|
||||||
|
ports = [{
|
||||||
|
internal = 8787
|
||||||
|
external = 8787
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for readarr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8787
|
||||||
|
endpoint = "http://${local.container_name}:8787"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "sabnzbd"
|
||||||
|
image = "lscr.io/linuxserver/sabnzbd"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
sabnzbd_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/sabnzbd/config"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = "${var.data_root}/usenet/downloads"
|
||||||
|
container_path = "/downloads"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
sabnzbd_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "sabnzbd" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.sabnzbd_volumes
|
||||||
|
env_vars = local.sabnzbd_env_vars
|
||||||
|
ports = [{
|
||||||
|
internal = 8080
|
||||||
|
external = 6789
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "unless-stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for integration with networking modules"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8080
|
||||||
|
endpoint = "http://${local.container_name}:8080"
|
||||||
|
subdomains = ["sabnzbd"]
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "sonarr"
|
||||||
|
image = "lscr.io/linuxserver/sonarr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
sonarr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = "${var.volume_path}/sonarr"
|
||||||
|
container_path = "/config"
|
||||||
|
read_only = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host_path = var.data_root
|
||||||
|
container_path = "/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
sonarr_env_vars = {
|
||||||
|
PUID = var.user_id
|
||||||
|
PGID = var.group_id
|
||||||
|
TZ = var.timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
sonarr_healthcheck = {
|
||||||
|
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8989/sonarr/ping"]
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "5s"
|
||||||
|
retries = 10
|
||||||
|
start_period = "5s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "sonarr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.sonarr_volumes
|
||||||
|
env_vars = local.sonarr_env_vars
|
||||||
|
healthcheck = local.sonarr_healthcheck
|
||||||
|
ports = [{
|
||||||
|
internal = 8989
|
||||||
|
external = 8989
|
||||||
|
protocol = "tcp"
|
||||||
|
}]
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for sonarr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
primary_port = 8989
|
||||||
|
endpoint = "http://${local.container_name}:8989"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the container"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "download_root" {
|
||||||
|
description = "Directory for downloads"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sonarr_api_key" {
|
||||||
|
description = "API key for Sonarr"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "radarr_api_key" {
|
||||||
|
description = "API key for Radarr"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of networks to which the container should be attached"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitoring" {
|
||||||
|
description = "Enable container monitoring"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
container_name = "unpackerr"
|
||||||
|
image = "golift/unpackerr"
|
||||||
|
tag = "latest"
|
||||||
|
|
||||||
|
unpackerr_volumes = [
|
||||||
|
{
|
||||||
|
host_path = var.download_root
|
||||||
|
container_path = "/data/torrents"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
unpackerr_env_vars = {
|
||||||
|
TZ = var.timezone
|
||||||
|
UN_SONARR_0_URL = "http://sonarr:8989/sonarr"
|
||||||
|
UN_SONARR_0_API_KEY = var.sonarr_api_key
|
||||||
|
UN_RADARR_0_URL = "http://radarr:7878/radarr"
|
||||||
|
UN_RADARR_0_API_KEY = var.radarr_api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
unpackerr_security_opts = [
|
||||||
|
"no-new-privileges:true"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module "unpackerr" {
|
||||||
|
source = "../../../../10-services-generic/docker-service"
|
||||||
|
container_name = local.container_name
|
||||||
|
image = local.image
|
||||||
|
tag = local.tag
|
||||||
|
volumes = local.unpackerr_volumes
|
||||||
|
env_vars = local.unpackerr_env_vars
|
||||||
|
security_opts = local.unpackerr_security_opts
|
||||||
|
networks = var.networks
|
||||||
|
monitoring = var.monitoring
|
||||||
|
restart_policy = "always"
|
||||||
|
user = "${var.user_id}:${var.group_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_definition" {
|
||||||
|
description = "Service definition for unpackerr"
|
||||||
|
value = {
|
||||||
|
name = local.container_name
|
||||||
|
endpoint = "http://${local.container_name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
modules/20-services-apps/media-server/variables.tf
Normal file
55
modules/20-services-apps/media-server/variables.tf
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
variable "volume_path" {
|
||||||
|
description = "Base directory for volumes (APP_DATA)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "data_root" {
|
||||||
|
description = "Root directory for media data (DATA_ROOT)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "download_root" {
|
||||||
|
description = "Directory for downloads (DOWNLOAD_ROOT)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_id" {
|
||||||
|
description = "User ID for container permissions"
|
||||||
|
type = string
|
||||||
|
default = "1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "group_id" {
|
||||||
|
description = "Group ID for container permissions"
|
||||||
|
type = string
|
||||||
|
default = "1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "timezone" {
|
||||||
|
description = "Timezone for the containers"
|
||||||
|
type = string
|
||||||
|
default = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hostname" {
|
||||||
|
description = "Hostname for the Jellyfin PublishedServerUrl"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sonarr_api_key" {
|
||||||
|
description = "API key for Sonarr"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "radarr_api_key" {
|
||||||
|
description = "API key for Radarr"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "List of additional networks to which containers should be attached"
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
108
modules/20-services-apps/n8n/README.md
Normal file
108
modules/20-services-apps/n8n/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# n8n Module
|
||||||
|
|
||||||
|
This module deploys [n8n](https://n8n.io/), a workflow automation tool for technical people, as Docker containers in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The n8n module:
|
||||||
|
|
||||||
|
- Deploys two Docker containers:
|
||||||
|
- `n8n`: The main workflow automation server
|
||||||
|
- `n8n-postgres`: A PostgreSQL database backend
|
||||||
|
- Creates a dedicated Docker network (`n8n-network`) for container communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "n8n" {
|
||||||
|
source = "./modules/20-services-apps/n8n"
|
||||||
|
volume_path = "/path/to/volumes/n8n"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the n8n image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for n8n and Postgres data volumes | `string` | - |
|
||||||
|
| `networks` | List of additional networks to which n8n should be attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "n8n"
|
||||||
|
primary_port = 5678
|
||||||
|
endpoint = "http://n8n:5678"
|
||||||
|
subdomains = ["n8n"]
|
||||||
|
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
n8n requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider:
|
||||||
|
|
||||||
|
- Database configuration:
|
||||||
|
- `POSTGRES_USER`: Root PostgreSQL user
|
||||||
|
- `POSTGRES_PASSWORD`: Root PostgreSQL password
|
||||||
|
- `POSTGRES_DB`: Database name for n8n
|
||||||
|
- `POSTGRES_NON_ROOT_USER`: Non-root user for n8n to connect with
|
||||||
|
- `POSTGRES_NON_ROOT_PASSWORD`: Password for the non-root user
|
||||||
|
|
||||||
|
- n8n configuration:
|
||||||
|
- `N8N_HOST`: Host for n8n to use
|
||||||
|
- `N8N_PORT`: Port for n8n to use
|
||||||
|
- `N8N_PROTOCOL`: Protocol for n8n (http or https)
|
||||||
|
- `WEBHOOK_URL`: URL for webhooks
|
||||||
|
- `NODE_FUNCTION_ALLOW_EXTERNAL`: Whether to allow external function calls
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
n8n stores its data in two main volumes:
|
||||||
|
|
||||||
|
1. n8n application data: `/home/node/.n8n` in the container, mapped to `${volume_path}/n8n_storage/_data` on the host
|
||||||
|
2. PostgreSQL data: `/var/lib/postgresql/data` in the container, mapped to `${volume_path}/db_storage/_data` on the host
|
||||||
|
|
||||||
|
Additionally, an initialization script is mounted to the PostgreSQL container:
|
||||||
|
- `/docker-entrypoint-initdb.d/init-data.sh` in the container, from `${volume_path}/init-data.sh` on the host
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The module creates a dedicated Docker network named `n8n-network` for communication between the n8n and PostgreSQL containers. The n8n container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "n8n" {
|
||||||
|
source = "./modules/20-services-apps/n8n"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.n8n.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -129,5 +129,6 @@ output "service_definition" {
|
|||||||
primary_port = local.n8n_internal_port
|
primary_port = local.n8n_internal_port
|
||||||
endpoint = "http://${local.container_name}:${local.n8n_internal_port}"
|
endpoint = "http://${local.container_name}:${local.n8n_internal_port}"
|
||||||
subdomains = ["n8n"]
|
subdomains = ["n8n"]
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
modules/20-services-apps/ntfy/README.md
Normal file
81
modules/20-services-apps/ntfy/README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# NTFY Module
|
||||||
|
|
||||||
|
This module deploys [NTFY](https://ntfy.sh/), a simple HTTP-based pub-sub notification service, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The NTFY module:
|
||||||
|
|
||||||
|
- Deploys the `binwiederhier/ntfy` Docker container
|
||||||
|
- Persists configuration and cache data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "ntfy" {
|
||||||
|
source = "./modules/20-services-apps/ntfy"
|
||||||
|
volume_path = "/path/to/volumes/ntfy"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the NTFY image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for NTFY data volumes | `string` | - |
|
||||||
|
| `networks` | List of networks to which the container should be attached | `list(string)` | - |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "ntfy"
|
||||||
|
primary_port = 80
|
||||||
|
endpoint = "http://ntfy:80"
|
||||||
|
subdomains = ["ntfy"]
|
||||||
|
publish_via = "reverse_proxy" # Expose via Caddy reverse proxy
|
||||||
|
proxied = true # Proxy through Cloudflare
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
NTFY stores its data in two volumes:
|
||||||
|
|
||||||
|
1. Configuration: `/etc/ntfy` in the container, mapped to `${volume_path}/app` on the host
|
||||||
|
2. Cache data: `/var/cache/ntfy` in the container, mapped to `${volume_path}/cache` on the host
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`. The `proxied = true` setting ensures that the DNS record is proxied through Cloudflare.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "ntfy" {
|
||||||
|
source = "./modules/20-services-apps/ntfy"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.ntfy.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -51,6 +51,8 @@ output "service_definition" {
|
|||||||
name = local.container_name
|
name = local.container_name
|
||||||
primary_port = local.exposed_port
|
primary_port = local.exposed_port
|
||||||
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
endpoint = "http://${local.container_name}:${local.exposed_port}"
|
||||||
subdomains = local.subdomains
|
subdomains = local.subdomains
|
||||||
|
publish_via = "reverse_proxy"
|
||||||
|
proxied = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
101
modules/20-services-apps/pterodactyl/README.md
Normal file
101
modules/20-services-apps/pterodactyl/README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Pterodactyl Module
|
||||||
|
|
||||||
|
This module is a parent module for deploying the [Pterodactyl](https://pterodactyl.io/) game server management system, which consists of multiple components:
|
||||||
|
|
||||||
|
1. **Panel** - The web-based administration interface and API server
|
||||||
|
2. **Wings** - The game server agent that controls individual game servers
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Pterodactyl module consists of two submodules:
|
||||||
|
|
||||||
|
- `panel` - Deploys the Pterodactyl control panel with its database and cache servers
|
||||||
|
- `wings` - Deploys the Pterodactyl Wings agent for running game servers
|
||||||
|
|
||||||
|
For a complete installation, both components should be deployed.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Pterodactyl is designed with a client-server architecture:
|
||||||
|
|
||||||
|
- **Panel (Server)**: The central management interface where administrators create servers, manage users, and configure settings.
|
||||||
|
- **Wings (Agent)**: Installed on each machine that will run game servers, communicates with the Panel via API.
|
||||||
|
|
||||||
|
In a homelab environment, you might deploy both components on the same machine or separate them for better resource allocation.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Deploying Both Components
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "pterodactyl_panel" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/panel"
|
||||||
|
volume_path = "${var.volume_host}/pterodactyl/panel"
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
module "pterodactyl_wings" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/wings"
|
||||||
|
volume_path = "${var.volume_host}/pterodactyl/wings"
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include both service definitions in your networking modules
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.pterodactyl_panel.service_definition,
|
||||||
|
module.pterodactyl_wings.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Requirements
|
||||||
|
|
||||||
|
### Panel Setup
|
||||||
|
|
||||||
|
1. Create a `.env` file in the panel module directory with required variables:
|
||||||
|
- Database credentials (`MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD`, etc.)
|
||||||
|
- App settings (`APP_URL`, `APP_TIMEZONE`, etc.)
|
||||||
|
- CORS and proxy settings
|
||||||
|
|
||||||
|
2. SMTP settings are sourced from the global SMTP module
|
||||||
|
|
||||||
|
### Wings Setup
|
||||||
|
|
||||||
|
1. After deploying the Panel, you need to:
|
||||||
|
- Create a node in the Panel UI
|
||||||
|
- Download the wings configuration from the Panel
|
||||||
|
- Place it at `${volume_path}/etc/config.yml` for the Wings module
|
||||||
|
|
||||||
|
## Network Configuration
|
||||||
|
|
||||||
|
Both components create their own dedicated Docker networks:
|
||||||
|
|
||||||
|
- `ptero-panel`: For communication between Panel, database, and cache
|
||||||
|
- `ptero-wings`: For communication between Wings and game servers
|
||||||
|
|
||||||
|
Additionally, both components need to be connected to your main homelab network to communicate with each other.
|
||||||
|
|
||||||
|
## Service Definitions
|
||||||
|
|
||||||
|
Both components generate service definitions that can be used by your networking modules:
|
||||||
|
|
||||||
|
- Panel: Published on the domain `gameservers.yourdomain.com`
|
||||||
|
- Wings: Published on the domain `wings.yourdomain.com`
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Wings requires `privileged` mode to create game server containers
|
||||||
|
- Panel communicates with Wings via API using a token configured in the wings config.yml
|
||||||
|
|
||||||
|
## Additional Documentation
|
||||||
|
|
||||||
|
For more detailed information about each component, please see:
|
||||||
|
|
||||||
|
- [Panel README](/modules/20-services-apps/pterodactyl/panel/README.md)
|
||||||
|
- [Wings README](/modules/20-services-apps/pterodactyl/wings/README.md)
|
||||||
|
|
||||||
|
For official Pterodactyl documentation, visit [https://pterodactyl.io/](https://pterodactyl.io/)
|
||||||
109
modules/20-services-apps/pterodactyl/panel/README.md
Normal file
109
modules/20-services-apps/pterodactyl/panel/README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Pterodactyl Panel Module
|
||||||
|
|
||||||
|
This module deploys [Pterodactyl Panel](https://pterodactyl.io/), a game server management panel, as Docker containers in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Pterodactyl Panel module:
|
||||||
|
|
||||||
|
- Deploys three Docker containers:
|
||||||
|
- `pterodactyl-panel`: The main web UI and API server
|
||||||
|
- `pterodactyl-db`: A MariaDB database backend
|
||||||
|
- `pterodactyl-cache`: A Redis cache server
|
||||||
|
- Creates a dedicated Docker network (`ptero-panel`) for container communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "pterodactyl_panel" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/panel"
|
||||||
|
volume_path = "/path/to/volumes/pterodactyl/panel"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the Pterodactyl Panel image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for Pterodactyl Panel volumes | `string` | - |
|
||||||
|
| `networks` | List of networks to which the panel should be attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "pterodactyl-panel"
|
||||||
|
primary_port = 80
|
||||||
|
endpoint = "http://pterodactyl-panel:80"
|
||||||
|
subdomains = ["gameservers"]
|
||||||
|
publish_via = "tunnel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Pterodactyl Panel requires several environment variables to function properly. These are stored in a `.env` file in the module directory and read using the `dotenv` Terraform provider. Key variables include:
|
||||||
|
|
||||||
|
- Panel Configuration:
|
||||||
|
- `APP_URL`: The URL where the panel will be accessed
|
||||||
|
- `APP_TIMEZONE`: The timezone for the application
|
||||||
|
- `APP_SERVICE_AUTHOR`: Service author information
|
||||||
|
|
||||||
|
- Database Configuration:
|
||||||
|
- `MYSQL_PASSWORD`: Database password
|
||||||
|
- `MYSQL_ROOT_PASSWORD`: Database root password
|
||||||
|
- `MYSQL_DATABASE`: Database name
|
||||||
|
- `MYSQL_USER`: Database username
|
||||||
|
|
||||||
|
- Mail Configuration:
|
||||||
|
- Mail settings are automatically sourced from the global SMTP module
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
Pterodactyl Panel stores its data in multiple volumes:
|
||||||
|
|
||||||
|
1. Application data: `/app/var` in the container, mapped to `${volume_path}/var` on the host
|
||||||
|
2. Nginx configuration: `/etc/nginx/http.d` in the container, mapped to `${volume_path}/nginx` on the host
|
||||||
|
3. SSL certificates: `/etc/letsencrypt` in the container, mapped to `${volume_path}/certs` on the host
|
||||||
|
4. Logs: `/app/storage/logs` in the container, mapped to `${volume_path}/logs` on the host
|
||||||
|
5. Database data: `/var/lib/mysql` in the MariaDB container, mapped to `${volume_path}/database` on the host
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The module creates a dedicated Docker network named `ptero-panel` for communication between the panel, database, and cache containers. The panel container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab.
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "pterodactyl_panel" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/panel"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.pterodactyl_panel.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -159,5 +159,6 @@ output "service_definition" {
|
|||||||
primary_port = 80
|
primary_port = 80
|
||||||
endpoint = "http://${local.container_name}:80"
|
endpoint = "http://${local.container_name}:80"
|
||||||
subdomains = ["gameservers"]
|
subdomains = ["gameservers"]
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
100
modules/20-services-apps/pterodactyl/wings/README.md
Normal file
100
modules/20-services-apps/pterodactyl/wings/README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Pterodactyl Wings Module
|
||||||
|
|
||||||
|
This module deploys [Pterodactyl Wings](https://pterodactyl.io/wings/), the game server agent component of Pterodactyl, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Pterodactyl Wings module:
|
||||||
|
|
||||||
|
- Deploys the `pterodactyl-wings` Docker container
|
||||||
|
- Creates a dedicated Docker network (`ptero-wings`) for game server communication
|
||||||
|
- Persists data to volumes on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
- Runs with privileged mode to manage game server containers
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "pterodactyl_wings" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/wings"
|
||||||
|
volume_path = "/path/to/volumes/pterodactyl/wings"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ------------------------------------------------------- | -------------- | ----------- |
|
||||||
|
| `image_tag` | Tag of the Pterodactyl Wings image to use | `string` | `"v1.11.3"` |
|
||||||
|
| `volume_path` | Host path for Pterodactyl Wings volumes | `string` | - |
|
||||||
|
| `networks` | List of networks to which wings should be attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "pterodactyl-wings"
|
||||||
|
primary_port = 443
|
||||||
|
endpoint = "http://pterodactyl-wings:443"
|
||||||
|
subdomains = ["wings"]
|
||||||
|
publish_via = "tunnel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Pterodactyl Wings uses the following environment variables:
|
||||||
|
|
||||||
|
- `TZ`: Timezone (set to Australia/Brisbane)
|
||||||
|
- `WINGS_UID`: User ID for wings process (988)
|
||||||
|
- `WINGS_GID`: Group ID for wings process (988)
|
||||||
|
- `WINGS_USERNAME`: Username for wings process ("pterodactyl")
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
Pterodactyl Wings uses several volume mounts:
|
||||||
|
|
||||||
|
1. Docker socket: `/var/run/docker.sock` (for controlling game server containers)
|
||||||
|
2. Docker containers: `/var/lib/docker/containers/` (for accessing container information)
|
||||||
|
3. SSL certificates: `/etc/ssl/certs` (mounted read-only)
|
||||||
|
4. Wings configuration: `/etc/pterodactyl/` in the container, mapped to `${volume_path}/etc`
|
||||||
|
5. Wings data: `/var/lib` in the container, mapped to `${volume_path}/var/lib`
|
||||||
|
6. Logs: `/var/log/pterodactyl/` in the container, mapped to `${volume_path}/var/log`
|
||||||
|
7. Temporary files: `${volume_path}/tmp` in the container and host
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
The module creates a dedicated Docker network named `ptero-wings` for game server communication. This network is configured with the subnet `172.21.0.0/16` and is made attachable to allow game server containers to connect to it. The wings container is also attached to any additional networks specified in the `networks` variable.
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "pterodactyl_wings" {
|
||||||
|
source = "./modules/20-services-apps/pterodactyl/wings"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.pterodactyl_wings.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -55,7 +55,7 @@ locals {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
host_path = "${var.volume_path}/var/lib"
|
host_path = "${var.volume_path}/var/lib"
|
||||||
container_path = "/var/lib/pterodactyl/"
|
container_path = "${var.volume_path}/var/lib"
|
||||||
read_only = false
|
read_only = false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,7 +65,7 @@ locals {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
host_path = "${var.volume_path}/tmp"
|
host_path = "${var.volume_path}/tmp"
|
||||||
container_path = "/tmp/pterodactyl/"
|
container_path = "${var.volume_path}/tmp"
|
||||||
read_only = false
|
read_only = false
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -86,7 +86,7 @@ module "wings_network" {
|
|||||||
name = "ptero-wings"
|
name = "ptero-wings"
|
||||||
driver = "bridge"
|
driver = "bridge"
|
||||||
attachable = true
|
attachable = true
|
||||||
subnet = "172.32.0.0/16"
|
subnet = "172.21.0.0/16"
|
||||||
options = {
|
options = {
|
||||||
"com.docker.network.bridge.name" = "ptero-wings"
|
"com.docker.network.bridge.name" = "ptero-wings"
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,8 @@ module "wings" {
|
|||||||
container_name = local.container_name
|
container_name = local.container_name
|
||||||
image = local.image
|
image = local.image
|
||||||
tag = local.image_tag
|
tag = local.image_tag
|
||||||
|
pgid = 988
|
||||||
|
puid = 988
|
||||||
volumes = local.volumes
|
volumes = local.volumes
|
||||||
env_vars = local.env_vars
|
env_vars = local.env_vars
|
||||||
networks = concat([module.wings_network.name], var.networks)
|
networks = concat([module.wings_network.name], var.networks)
|
||||||
@@ -111,5 +113,6 @@ output "service_definition" {
|
|||||||
primary_port = 443
|
primary_port = 443
|
||||||
endpoint = "http://${local.container_name}:443"
|
endpoint = "http://${local.container_name}:443"
|
||||||
subdomains = local.subdomains
|
subdomains = local.subdomains
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
modules/20-services-apps/searxng/README.md
Normal file
79
modules/20-services-apps/searxng/README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# SearxNG Module
|
||||||
|
|
||||||
|
This module deploys [SearxNG](https://searx.github.io/searx/), a privacy-respecting metasearch engine, as a Docker container in the homelab environment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The SearxNG module:
|
||||||
|
|
||||||
|
- Deploys the `searxng/searxng` Docker container
|
||||||
|
- Persists configuration data to a volume on the host
|
||||||
|
- Provides service definition for integration with networking modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "searxng" {
|
||||||
|
source = "./modules/20-services-apps/searxng"
|
||||||
|
volume_path = "/path/to/volumes/searxng"
|
||||||
|
networks = ["homelab-network"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
| Variable | Description | Type | Default |
|
||||||
|
| ------------- | ---------------------------------------------------------- | -------------- | ---------- |
|
||||||
|
| `image_tag` | Tag of the SearxNG image to use | `string` | `"latest"` |
|
||||||
|
| `volume_path` | Host path for SearxNG configuration volume | `string` | - |
|
||||||
|
| `networks` | List of networks to which the container should be attached | `list(string)` | `[]` |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
| -------------------- | --------------------------------------------- |
|
||||||
|
| `service_definition` | Service definition for integration with networking modules |
|
||||||
|
|
||||||
|
## Service Definition
|
||||||
|
|
||||||
|
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
{
|
||||||
|
name = "searxng"
|
||||||
|
primary_port = 8080
|
||||||
|
endpoint = "http://searxng:8080"
|
||||||
|
subdomains = ["search"]
|
||||||
|
publish_via = "tunnel" # Only publish through Cloudflare tunnel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
SearxNG stores its configuration in a single volume:
|
||||||
|
|
||||||
|
- Configuration: `/etc/searxng` in the container, mapped to `${volume_path}/config` on the host
|
||||||
|
|
||||||
|
## Integration with Networking Modules
|
||||||
|
|
||||||
|
This service is configured to be exposed through a Cloudflare tunnel for secure remote access, set by `publish_via = "tunnel"`.
|
||||||
|
|
||||||
|
## Example Integration in Main Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "searxng" {
|
||||||
|
source = "./modules/20-services-apps/searxng"
|
||||||
|
volume_path = module.system_globals.volume_host
|
||||||
|
networks = [module.services.homelab_docker_network_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The service definition is automatically included in the services output
|
||||||
|
module "services" {
|
||||||
|
source = "./modules/services"
|
||||||
|
# ...
|
||||||
|
service_definitions = [
|
||||||
|
module.searxng.service_definition,
|
||||||
|
# Other service definitions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -55,5 +55,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 = ["search"]
|
subdomains = ["search"]
|
||||||
|
publish_via = "tunnel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ module "actualbudget" {
|
|||||||
networks = [module.homelab_docker_network.name]
|
networks = [module.homelab_docker_network.name]
|
||||||
}
|
}
|
||||||
|
|
||||||
module "coolify" {
|
module "affine" {
|
||||||
source = "${local.module_dir}/20-services-apps/coolify"
|
source = "${local.module_dir}/20-services-apps/affine"
|
||||||
volume_path = "${local.volume_host}/coolify"
|
volume_path = "${local.volume_host}/affine"
|
||||||
|
networks = [module.homelab_docker_network.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ output "service_definitions" {
|
|||||||
description = "Service definitions for all services"
|
description = "Service definitions for all services"
|
||||||
value = [
|
value = [
|
||||||
module.actualbudget.service_definition,
|
module.actualbudget.service_definition,
|
||||||
|
module.affine.service_definition,
|
||||||
|
module.calibre.service_definition,
|
||||||
module.emulatorjs.service_definition,
|
module.emulatorjs.service_definition,
|
||||||
module.linkwarden.service_definition,
|
module.linkwarden.service_definition,
|
||||||
module.ntfy.service_definition,
|
module.ntfy.service_definition,
|
||||||
|
|||||||
Reference in New Issue
Block a user