commit 2fb1c45e87b977a03f6c977d86229b7cbb03b251 Author: Jeroen Vijgen Date: Wed Aug 6 14:23:17 2025 +0000 Start creating new homelab settings with OpenTofu diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76e7103 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.terraform/ +*.tfstate +*.tfstate.* +crash.log +*.tfvars +override.tf +override.tf.json +.terraformrc +terraform.rc +.env \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..59b39a9 --- /dev/null +++ b/main.tf @@ -0,0 +1,23 @@ +module "system_globals" { + source = "./modules/00-globals/system" +} + +// Application services +module "services" { + source = "./services" +} + +module "caddy" { + source = "./modules/01-networking/caddy-service" + volume_path = "./docker/infrastructure/" + domains = [ + "blackchaosnl.duckdns.org", + "blackchaosnl.myaddr.io", + "blackchaosnl.myaddr.dev", + "blackchaosnl.myaddr.tools" + ] + tls_email = "your-email@example.com" # For Let's Encrypt + container_name = "caddy" + service_definitions = module.services.service_definitions + networks = ["default"] +} \ No newline at end of file diff --git a/modules/00-globals/system/main.tf b/modules/00-globals/system/main.tf new file mode 100644 index 0000000..664e29d --- /dev/null +++ b/modules/00-globals/system/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + dotenv = { + source = "germanbrew/dotenv" + } + } +} + +data "dotenv" "system_config" {} + +// Outputs +output "timezone" { + description = "System timezone" + value = data.dotenv.system_config.entries.TIMEZONE +} + +output "volume_host" { + description = "Base directory for host volumes" + value = data.dotenv.system_config.entries.VOLUME_HOST +} + +output "puid" { + description = "PUID for Docker containers" + value = data.dotenv.system_config.entries.PUID +} + +output "pgid" { + description = "PGID for Docker containers" + value = data.dotenv.system_config.entries.PGID +} \ No newline at end of file diff --git a/modules/00-globals/tls/main.tf b/modules/00-globals/tls/main.tf new file mode 100644 index 0000000..dcfe208 --- /dev/null +++ b/modules/00-globals/tls/main.tf @@ -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 +} \ No newline at end of file diff --git a/modules/01-networking/caddy-service/README.md b/modules/01-networking/caddy-service/README.md new file mode 100644 index 0000000..4d3943b --- /dev/null +++ b/modules/01-networking/caddy-service/README.md @@ -0,0 +1,126 @@ +# 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" + domains = ["yourdomain.com"] + tls_email = "your-email@example.com" # For Let's Encrypt + container_name = "caddy-proxy" + service_definitions = module.services.service_definitions + networks = ["your-docker-network"] +} +``` + +### 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 + + # 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 +} +``` + +## 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"` | +| `domains` | The domain names to use for services | `list(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)` | `[]` | + +## 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"] + } +} +``` + +### 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"] + 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 + } +} +``` \ No newline at end of file diff --git a/modules/01-networking/caddy-service/main.tf b/modules/01-networking/caddy-service/main.tf new file mode 100644 index 0000000..ab6c0ea --- /dev/null +++ b/modules/01-networking/caddy-service/main.tf @@ -0,0 +1,141 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + } + } +} + +locals { + container_name = var.container_name != "" ? var.container_name : "caddy" + 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 + ] + + // Transform service definitions into Caddyfile blocks + caddy_site_configs = flatten([ + for service in local.proxy_services : + [ + for domain in var.domains : [ + for subdomain in service.subdomains : { + site_address = "${subdomain}.${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 + } + ] + ] + ]) + + caddyfile_default = <<-EOT + { + email ${var.tls_email} + + log { + format console + output stdout + } + } + + (headers) { + header { + -server + -via + + Permissions-Policy interest-cohort=() + + Strict-Transport-Security "max-age=31536000; includesSubDomains; preload" + + X-Content-Type-Options "nosniff" + + X-Frame-Options DENY + } + } + + EOT + + // Generate the main Caddyfile content + caddyfile_content = merge(local.caddyfile_default, 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} { + import headers + ${site.custom_config} + } + EOT + : + // Otherwise use the standard reverse proxy config with options + <<-EOT + ${site.site_address} { + import headers + 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 "caddy" { + source = "../../10-services-generic/docker-service" + + container_name = local.container_name + image = "caddy" + tag = local.image_tag + + volumes = [ + { + host_path = "${var.volume_path}/${image}/data" + container_path = "/data" + read_only = false + }, + { + host_path = "${var.volume_path}/${image}/config" + container_path = "/config" + read_only = false + }, + { + host_path = "${var.volume_path}/${image}/Caddyfile" + container_path = "/etc/caddy/Caddyfile" + read_only = true + } + ] + + ports = [ + { + external = "80" + internal = "80" + protocol = "tcp" + }, + { + external = "443" + internal = "443" + protocol = "tcp" + } + ] + + networks = var.networks +} \ No newline at end of file diff --git a/modules/01-networking/caddy-service/outputs.tf b/modules/01-networking/caddy-service/outputs.tf new file mode 100644 index 0000000..38856d4 --- /dev/null +++ b/modules/01-networking/caddy-service/outputs.tf @@ -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 + } +} \ No newline at end of file diff --git a/modules/01-networking/caddy-service/variables.tf b/modules/01-networking/caddy-service/variables.tf new file mode 100644 index 0000000..095cfdc --- /dev/null +++ b/modules/01-networking/caddy-service/variables.tf @@ -0,0 +1,46 @@ +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 "domains" { + description = "Which domain name to use for services" + type = list(object({ + name = 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), {}) + })) +} + +variable "networks" { + description = "List of Docker networks to connect the Caddy container to" + type = list(string) + default = [] +} diff --git a/modules/01-networking/network-service/main.tf b/modules/01-networking/network-service/main.tf new file mode 100644 index 0000000..53bd385 --- /dev/null +++ b/modules/01-networking/network-service/main.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + } + } +} + +resource "docker_network" "this" { + name = var.name + driver = var.driver + internal = var.internal + attachable = var.attachable + ipam_driver = var.ipam_driver + + dynamic "ipam_config" { + for_each = var.subnet != "" ? [1] : [] + content { + subnet = var.subnet + gateway = var.gateway + ip_range = var.ip_range + aux_address = var.aux_address + } + } + + options = var.options +} \ No newline at end of file diff --git a/modules/01-networking/network-service/outputs.tf b/modules/01-networking/network-service/outputs.tf new file mode 100644 index 0000000..c80c52b --- /dev/null +++ b/modules/01-networking/network-service/outputs.tf @@ -0,0 +1,19 @@ +output "network_id" { + description = "The ID of the Docker network" + value = docker_network.this.id +} + +output "name" { + description = "The name of the Docker network" + value = docker_network.this.name +} + +output "network_driver" { + description = "The driver of the Docker network" + value = docker_network.this.driver +} + +output "ipam_config" { + description = "The IPAM configuration of the Docker network" + value = docker_network.this.ipam_config +} \ No newline at end of file diff --git a/modules/01-networking/network-service/variables.tf b/modules/01-networking/network-service/variables.tf new file mode 100644 index 0000000..2520a70 --- /dev/null +++ b/modules/01-networking/network-service/variables.tf @@ -0,0 +1,64 @@ +variable "name" { + description = "Name of the Docker network" + type = string +} + +variable "driver" { + description = "Name of the network driver to use" + type = string + default = "bridge" +} + +variable "internal" { + description = "Restrict external access to the network if true" + type = bool + default = false +} + +variable "attachable" { + description = "Enable manual container attachment if true" + type = bool + default = true +} + +variable "ipam_driver" { + description = "Driver used for IP address management" + type = string + default = "default" +} + +variable "subnet" { + description = "Subnet in CIDR format that represents a network segment" + type = string + default = "" +} + +variable "gateway" { + description = "IPv4 or IPv6 gateway for the subnet" + type = string + default = "" +} + +variable "ip_range" { + description = "Range of IPs from which to allocate container IPs" + type = string + default = "" +} + +variable "aux_address" { + description = "Auxiliary IPv4 or IPv6 addresses used by the driver" + type = map(string) + default = {} +} + +variable "labels" { + description = "Labels to add to the network" + type = map(string) + default = {} +} + +variable "options" { + description = "Network driver specific options" + type = map(string) + default = {} +} \ No newline at end of file diff --git a/modules/10-generic/docker-service/README.md b/modules/10-generic/docker-service/README.md new file mode 100644 index 0000000..b5e1edf --- /dev/null +++ b/modules/10-generic/docker-service/README.md @@ -0,0 +1,88 @@ +# Generic Docker Service Module + +This is a reusable OpenTofu module for deploying Docker containers with configurable options. It serves as the foundation for specific application modules in this homelab project. + +## Features + +- Pull and manage Docker images +- Configure container networking, ports, and volumes +- Set environment variables and labels +- Configure resource limits and constraints +- Set up health checks +- Support for container logging options + +## Usage + +This module is typically called by application-specific modules rather than used directly, but can be used as follows: + +```hcl +module "my_service" { + source = "../../10-services-generic/docker-service" + + container_name = "my-service" + image = "organization/image" + tag = "latest" + + restart_policy = "unless-stopped" + network_mode = "bridge" + + // Port mappings + ports = [ + { + internal = 8080 + external = 8080 + protocol = "tcp" + } + ] + + // Volume mappings + volumes = [ + { + host_path = "/path/on/host" + container_path = "/path/in/container" + read_only = false + } + ] + + // Environment variables + env_vars = { + VARIABLE_NAME = "value" + } + + // Container labels + labels = { + "com.example.description" = "My service description" + } +} +``` + +## Required Providers + +This module requires the Docker provider to be configured in your root module: + +```hcl +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + } + dotenv = { + source = "germanbrew/dotenv" + } + } +} +``` + +## Inputs + +See the `variables.tf` file for a complete list of input variables and their descriptions. + +## Outputs + +| Name | Description | +| --------------- | ------------------------------------------- | +| container_name | Name of the Docker container | +| container_id | ID of the Docker container | +| image_id | ID of the Docker image | +| ip_address | IP address of the container (if applicable) | +| container_ports | Published ports of the container | \ No newline at end of file diff --git a/modules/10-generic/docker-service/main.tf b/modules/10-generic/docker-service/main.tf new file mode 100644 index 0000000..1a35eeb --- /dev/null +++ b/modules/10-generic/docker-service/main.tf @@ -0,0 +1,150 @@ +// Generic Docker service module +// Creates and manages a Docker container with configurable options +module "system_globals" { + source = "../../00-globals/system" +} + +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + } + dotenv = { + source = "germanbrew/dotenv" + } + } +} + +locals { + network_mode = var.network_mode + container_name = var.container_name + image_name = "${var.image}:${var.tag}" + + default_env_vars = { + TZ = module.system_globals.timezone + PUID = var.puid != null ? var.puid : module.system_globals.puid + PGID = var.pgid != null ? var.pgid : module.system_globals.pgid + } + + env_vars = merge(var.env_vars, local.default_env_vars) + + // Prepare ports configuration + ports_config = [ + for port in var.ports : { + internal = port.internal + external = port.external + protocol = port.protocol + } + ] + + // Prepare volumes configuration + volumes_config = [ + for volume in var.volumes : { + host_path = volume.host_path + container_path = volume.container_path + read_only = volume.read_only + } + ] + + // Merge provided labels with monitoring labels + merged_labels = merge(var.labels) +} + +// Pull the Docker image +resource "docker_image" "service_image" { + name = local.image_name + keep_locally = var.keep_image_locally + pull_triggers = [var.tag] +} + +// Create the Docker container +resource "docker_container" "service_container" { + name = local.container_name + image = docker_image.service_image.image_id + + restart = var.restart_policy + + # Set the network mode (bridge, host, etc.) + network_mode = local.network_mode + + # Add host mappings (entries for /etc/hosts) + dynamic "host" { + for_each = var.host_mappings + content { + host = host.value.host + ip = host.value.ip + } + } + + # Dynamically configure ports based on the provided list + dynamic "ports" { + for_each = local.ports_config + content { + internal = ports.value.internal + external = ports.value.external + protocol = ports.value.protocol + } + } + + # Dynamically configure networks based on the provided list + dynamic "networks_advanced" { + for_each = var.networks + content { + name = networks_advanced.value + } + } + + # Dynamically configure volumes based on the provided list + dynamic "volumes" { + for_each = local.volumes_config + content { + host_path = volumes.value.host_path + container_path = volumes.value.container_path + read_only = volumes.value.read_only + } + } + + # Configure environment variables - map to array of strings + env = [for k, v in local.env_vars : "${k}=${v}"] + + # Set container labels + dynamic "labels" { + for_each = local.merged_labels + content { + label = labels.key + value = labels.value + } + } + + # Add container healthcheck if configured + dynamic "healthcheck" { + for_each = var.healthcheck != null ? [var.healthcheck] : [] + content { + test = healthcheck.value.test + interval = healthcheck.value.interval + timeout = healthcheck.value.timeout + start_period = healthcheck.value.start_period + retries = healthcheck.value.retries + } + } + + # Set resource limits if specified + memory = var.memory_limit + memory_swap = var.memory_swap_limit + cpu_shares = var.cpu_shares + + # Other container options + dns = var.dns + dns_search = var.dns_search + hostname = var.hostname + domainname = var.domainname + user = var.user + working_dir = var.working_dir + command = var.command + entrypoint = var.entrypoint + privileged = var.privileged + + # Set log options + log_driver = var.log_driver + log_opts = var.log_opts +} \ No newline at end of file diff --git a/modules/10-generic/docker-service/outputs.tf b/modules/10-generic/docker-service/outputs.tf new file mode 100644 index 0000000..04484d7 --- /dev/null +++ b/modules/10-generic/docker-service/outputs.tf @@ -0,0 +1,24 @@ +output "container_name" { + description = "Name of the Docker container" + value = docker_container.service_container.name +} + +output "container_id" { + description = "ID of the Docker container" + value = docker_container.service_container.id +} + +output "image_id" { + description = "ID of the Docker image" + value = docker_image.service_image.id +} + +output "ip_address" { + description = "IP address of the container (if applicable)" + value = docker_container.service_container.network_data != null ? docker_container.service_container.network_data[0].ip_address : null +} + +output "container_ports" { + description = "Published ports of the container" + value = docker_container.service_container.ports +} \ No newline at end of file diff --git a/modules/10-generic/docker-service/variables.tf b/modules/10-generic/docker-service/variables.tf new file mode 100644 index 0000000..25a306b --- /dev/null +++ b/modules/10-generic/docker-service/variables.tf @@ -0,0 +1,202 @@ +variable "container_name" { + description = "Name of the Docker container" + type = string +} + +variable "image" { + description = "Docker image name" + type = string +} + +variable "tag" { + description = "Docker image tag" + type = string + default = "latest" +} + +variable "keep_image_locally" { + description = "Whether to keep the Docker image locally after pulling" + type = bool + default = true +} + +variable "restart_policy" { + description = "Docker restart policy (no, always, unless-stopped, on-failure)" + type = string + default = "always" +} + +variable "network_mode" { + description = "Docker network mode (bridge, host, etc.)" + type = string + default = "bridge" +} + +variable "ports" { + description = "List of port mappings" + type = list(object({ + internal = number + external = number + protocol = string + })) + default = [] +} + +variable "networks" { + description = "List of networks to connect the container to" + type = list(string) + default = [] +} + +variable "volumes" { + description = "List of volume mappings" + type = list(object({ + host_path = string + container_path = string + read_only = bool + })) + default = [] +} + +variable "env_vars" { + description = "Environment variables for the container" + type = map(string) + default = {} + sensitive = true +} + +variable "puid" { + description = "User ID for the container" + type = number + default = null +} + +variable "pgid" { + description = "Group ID for the container" + type = number + default = null +} + +variable "labels" { + description = "Docker container labels" + type = map(string) + default = {} +} + +variable "managed_by_caddy" { + description = "Enable mounting container through labels" + type = bool + default = true +} + +variable "host_mappings" { + description = "Additional host mappings for the container (/etc/hosts entries)" + type = list(object({ + host = string + ip = string + })) + default = [] +} + +variable "healthcheck" { + description = "Container healthcheck configuration" + type = object({ + test = list(string) + interval = string + timeout = string + start_period = optional(string) + retries = number + }) + default = null +} + +// Resource limits +variable "memory_limit" { + description = "Memory limit for the container (in MB)" + type = number + default = null +} + +variable "memory_swap_limit" { + description = "Memory swap limit for the container (in MB)" + type = number + default = null +} + +variable "cpu_shares" { + description = "CPU shares for the container (relative weight)" + type = number + default = null +} + +// Networking options +variable "dns" { + description = "DNS servers for the container" + type = list(string) + default = null +} + +variable "dns_search" { + description = "DNS search domains for the container" + type = list(string) + default = null +} + +variable "hostname" { + description = "Container hostname" + type = string + default = null +} + +variable "domainname" { + description = "Container domainname" + type = string + default = null +} + +// Execution options +variable "user" { + description = "User to run commands as inside the container" + type = string + default = "" +} + +variable "working_dir" { + description = "Working directory inside the container" + type = string + default = null +} + +variable "command" { + description = "Command to run when starting the container" + type = list(string) + default = null +} + +variable "entrypoint" { + description = "Entrypoint for the container" + type = list(string) + default = null +} + +variable "privileged" { + description = "Run container in privileged mode" + type = bool + default = false +} + +// Logging options +variable "log_driver" { + description = "Log driver for the container" + type = string + default = "json-file" +} + +variable "log_opts" { + description = "Log driver options" + type = map(string) + default = { + max-size = "10m" + max-file = "3" + } +} \ No newline at end of file diff --git a/modules/20-services-entertainment/jellyfin/main.tf b/modules/20-services-entertainment/jellyfin/main.tf new file mode 100644 index 0000000..8917705 --- /dev/null +++ b/modules/20-services-entertainment/jellyfin/main.tf @@ -0,0 +1,92 @@ +terraform { + required_providers { + dotenv = { + source = "germanbrew/dotenv" + } + } +} + +variable "image_tag" { + description = "The tag for the JellyFin container image. Default: Latest" + 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 = "Europe/Helsinki" +} + +locals { + container_name = "jellyfin" + jellyfin_image = "docker.io/jellyfin/jellyfin" + jellyfin_tag = var.image_tag + env_file = "${path.module}/.env" + jellyfin_internal_port = 8096 + + jellyfin_volumes = [ + { + host_path = "/mnt/storage/media" + container_path = "/media" + read_only = true + }, + { + host_path = "${volume_path}/${container_name}/config" + container_path = "/config" + },{ + host_path = "${volume_path}/${container_name}/cache" + container_path = "/cache" + }, + ] + + jellyfin_env_vars = { + PUID = var.user_id + PGID = var.group_id + TZ = var.timezone + } +} + +module "jellyfin" { + source = "../../10-services-generic/docker-service" + container_name = local.container_name + image = local.jellyfin_image + tag = local.jellyfin_tag + volumes = local.jellyfin_volumes + env_vars = local.jellyfin_env_vars + networks = concat(var.networks) + restart_policy = "always" +} + +output "service_definition" { + description = "General service definition with optional ingress configuration" + value = { + name = local.container_name + primary_port = local.jellyfin_internal_port + endpoint = "http://${local.container_name}:${local.jellyfin_internal_port}" + subdomains = ["tv"] + } +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..07a403f --- /dev/null +++ b/providers.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + podman = { + source = "kreuzwerker/docker" + version = "~> 3.6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5.1" + } + dotenv = { + source = "germanbrew/dotenv" + version = "1.2.5" + } + } +} diff --git a/services/main.tf b/services/main.tf new file mode 100644 index 0000000..379e0f5 --- /dev/null +++ b/services/main.tf @@ -0,0 +1,17 @@ +locals { + module_dir = "../modules" + root_volume = module.system_globals.volume_host + volume_host = "${module.system_globals.volume_host}/appconfig" +} + +module "system_globals" { + source = "${local.module_dir}/00-globals/system" +} + +module "homelab_docker_network" { + source = "${local.module_dir}/01-networking/docker-network" + name = "default" + driver = "bridge" + attachable = true + subnet = "10.88.0.0/16" +} \ No newline at end of file diff --git a/services/outputs.tf b/services/outputs.tf new file mode 100644 index 0000000..f84e75d --- /dev/null +++ b/services/outputs.tf @@ -0,0 +1,26 @@ +output "service_definitions" { + description = "Service definitions for all services" + value = [ + module.actualbudget.service_definition, + module.affine.service_definition, + module.calibre.service_definition, + module.copyparty.service_definition, + module.crawl4ai.service_definition, + module.emulatorjs.service_definition, + module.glance.service_definition, + module.linkwarden.service_definition, + module.n8n.service_definition, + module.n8n.n8n_mcp_service_definition, + module.nocodb.service_definition, + module.ntfy.service_definition, + module.portainer.service_definition, + module.pterodactyl_wings.service_definition, + module.pterodactyl_panel.service_definition, + module.searxng.service_definition + ] +} + +output "homelab_docker_network_name" { + description = "The name of the Docker network" + value = module.homelab_docker_network.name +} \ No newline at end of file