first commit

This commit is contained in:
Yuris Cakranegara
2025-06-06 12:01:54 +10:00
commit cac26957a8
42 changed files with 2235 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.terraform/
*.tfstate
*.tfstate.*
crash.log
*.tfvars
override.tf
override.tf.json
.terraformrc
terraform.rc

64
.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,64 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.0"
constraints = "~> 4.0"
hashes = [
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.5.1"
constraints = "~> 3.5.1"
hashes = [
"h1:tW+G7lgqbHUtraKHPWuotYHlME1vcAf50YvOeHQlGHg=",
"zh:0002dd4c79453da5bf1bb9c52172a25d042a571f6df131b7c9ced3d1f8f3eb44",
"zh:49b0f8c2bd5632799aa6113e0e46acaa7d008f927665a41a1f8e8559fe6d8165",
"zh:56df70fca236caa06d0e636c41ab71dd1ced05375f4ddcb905b0ed2105737048",
"zh:58e4de40540c86b9e2e2595dac1318ba057718961a467fa9727866f747693eb2",
"zh:5992f11c738812ccd7476d4c607cb8b76dea5aa612be491150c89957ec395ddd",
"zh:7ff4f0b7707b51737f684e96d85a47f0dd8be0f72a3c27b0798755d3faad15e2",
"zh:8e4b0972e216c9773ab525accfa36eb27c44c751b06b125ecc53f4226c91cea8",
"zh:d8956cc5abcd5d1173b6cc25d5d8ed2c5cc456edab2fddb774a17d45e84820cb",
"zh:df7f9eb93a832e66bc20cc41c57d38954f87671ec60be09fa866273adb8d9353",
"zh:eb583d8f03b11f0b6c535375d8ed0d29e5f7f537b5c78943856d2e8ce76482d9",
]
}
provider "registry.opentofu.org/kreuzwerker/docker" {
version = "3.6.0"
constraints = ">= 3.0.0, ~> 3.6.0"
hashes = [
"h1:srHf1w7rgfYAis9tzEm9fJ6DNEpzTLDJxmJBk3yvEdo=",
"zh:1b930ce8aeeb562529383079a146ef6d8aa293a871490cabaf62cd07351f594a",
"zh:2b6285f977c5c2f43bd4640e634d3b74d32d7adf377824773e1ebf27b18d43c2",
"zh:31601af6e77cddc6c9dc9cdbcb53a18e38f35bcf4108ad4c88eb4b446b1a8100",
"zh:58b10d6923e89ac8bd7067760d5d115570ed29bd3b81c931f5ae09e2c5228d10",
"zh:62b76085922cb78370777c10286021104dde7ac7550f97bcbd5611cd7675db74",
"zh:76cacedffea6e07c35d649b79ad2a72dfe8f0c9d02c765a24162f3ebfef1f54d",
"zh:794d753e99319b42b746ba9405297049bb81b6e2d7c12496457bdf73e7d7cebe",
"zh:a8140eed5f3cf57aae4586471cf79a19babda57db9e4a29bd783e8b1fc12a2c0",
"zh:c62928bcdbf2965fd37d7b8fcb58b1e2bbecd9a393e579ac009439dbac3861cd",
"zh:c9b061d4b779cd9f833648a5220f35f027152e4ec499b7808c0e9b613dd70a87",
"zh:ce6418f00a732c9b1e2a1d250675820f7e2112923eea14e08b9dc82d187f8ea2",
"zh:df7f80e4f07d627ca2bb5586c62986e543ee89fab4c83105a8d485c3ec97007d",
"zh:e48a2416a48e22f5db90500791bc8deb72e110e141df685fe0e15266695629a9",
]
}

163
README.md Normal file
View File

@@ -0,0 +1,163 @@
# My OpenTofu Homelab Infrastructure
This project uses [OpenTofu](https://opentofu.org/) to manage the infrastructure for my personal homelab. It's designed to be modular and evolve, initially focusing on deploying Dockerized applications on a Debian server ("casa") and later expanding to include Proxmox VE for virtualization.
**Current Time:** June 5, 2025
## Table of Contents
1. [Overview](#overview)
2. [Prerequisites](#prerequisites)
3. [Project Structure](#project-structure)
4. [Configuration](#configuration)
5. [Usage](#usage)
6. [Module Overview](#module-overview)
7. [Future Plans](#future-plans)
## Overview
This OpenTofu configuration manages various self-hosted services primarily as Docker containers. The goals are:
* **Reproducibility:** Easily set up or replicate the homelab environment.
* **Version Control:** Track all infrastructure changes using Git.
* **Automation:** Automate the provisioning and management of services.
* **Modularity:** Organize infrastructure into reusable and understandable components.
## Prerequisites
Before you begin, ensure you have the following installed and configured:
* **OpenTofu:** Version `1.6.0` or higher (see `versions.tf`). [Installation Guide](https://opentofu.org/docs/intro/install/)
* **Git:** For version control.
* **Docker:** Installed and running on the target host(s) (initially "casa", later on VMs within Proxmox).
* **(Optional) Cloudflare Account:** If using the Cloudflare provider for DNS management or Tunnels. You'll need your Zone ID and an API Token.
* **(Future) Proxmox VE:** When moving to the virtualization phase, a Proxmox VE host will be required.
* **(Optional) Tailscale:** For secure remote access.
## Project Structure
The project is organized as follows:
```
homelab/
├── .gitignore # Files and directories to ignore
├── README.md # This file
├── main.tf # Root module: orchestrates module calls
├── variables.tf # Root module: global input variables
├── outputs.tf # Root module: global outputs
├── providers.tf # Root module: provider configurations
├── versions.tf # Root module: OpenTofu & provider version constraints
├── terraform.tfvars.example # Example variables file
├── environments/
│ ├── core/ # Core infrastructure (monitoring, globals)
│ ├── network/ # Network infrastructure (Docker networks, Cloudflare)
│ └── services/ # Application services (Docker containers)
└── modules/ # Local modules for different components
├── 00-globals/ # Optional: Global data sources/locals
├── 01-networking/
│ ├── docker-network/
│ ├── cloudflare-dns-record/
│ └── cloudflared-tunnel/
├── 02-compute/ # Future: Proxmox VM/LXC modules
│ └── proxmox-vm/
├── 10-services-generic/
│ └── docker-service/ # Generic module for deploying Docker containers
└── 20-services-apps/ # Application-specific wrapper modules
├── jellyfin/
├── affine/
└── ... # Other application modules
```
## Configuration
1. **Clone the repository:**
```bash
git clone https://github.com/yurisasc/homelab.git
cd homelab
```
2. **Provider Configuration:**
Review `providers.tf` and ensure provider configurations are suitable. For providers requiring authentication (like Cloudflare or Proxmox later), API tokens and other sensitive data should be supplied via variables.
3. **Create a `terraform.tfvars` file:**
Copy `terraform.tfvars.example` to `terraform.tfvars`:
```bash
cp terraform.tfvars.example terraform.tfvars
```
**Edit `terraform.tfvars` to set your specific values.** This file is included in `.gitignore` by default if it's expected to contain secrets.
Common variables to configure might include:
* `cloudflare_api_token`
* `cloudflare_zone_id`
* `docker_host_data_path_base` (e.g., `/srv/docker_data` on "casa")
* `domain_name` (e.g., `homelab.yourdomain.com`)
* Specific application settings (ports, image tags, paths for persistent data).
* (Future) Proxmox API details.
## Usage
Make sure you are in the root directory of the project (`homelab/`).
1. **Initialize OpenTofu:**
This downloads the necessary provider plugins. Run this once when you first set up the project or when you add/change providers or modules.
```bash
tofu init
```
2. **Plan Changes:**
This command shows you what OpenTofu will do to reach the desired state defined in your configuration files. Review the plan carefully.
```bash
tofu plan
# To use a specific .tfvars file:
# tofu plan -var-file="terraform.tfvars"
```
3. **Apply Changes:**
This command applies the changes outlined in the plan. You will be prompted for confirmation.
```bash
tofu apply
# To use a specific .tfvars file:
# tofu apply -var-file="terraform.tfvars"
# To auto-approve (use with caution):
# tofu apply -auto-approve -var-file="terraform.tfvars"
```
4. **View Outputs:**
If you have defined outputs in `outputs.tf` or in your modules, you can view them:
```bash
tofu output
```
5. **Destroy Infrastructure (Use with Extreme Caution!):**
This command will attempt to destroy all resources managed by this OpenTofu configuration.
```bash
tofu destroy
# To use a specific .tfvars file:
# tofu destroy -var-file="terraform.tfvars"
```
## Module Overview
This project aims for a high degree of modularity:
* **`modules/01-networking/`**: Contains modules for creating Docker networks, managing Cloudflare DNS records, and deploying `cloudflared` tunnels.
* **`modules/10-services-generic/docker-service/`**: A reusable module to deploy any generic Docker container with common configurations (ports, volumes, environment variables, etc.).
* **`modules/20-services-apps/`**: Contains "wrapper" modules for specific applications (e.g., Jellyfin, Affine, Nginx Proxy Manager). These modules typically call the generic `docker-service` module with pre-filled defaults and simpler inputs specific to that application.
* **`modules/02-compute/`**: (Planned for future Proxmox integration) Will contain modules for provisioning Virtual Machines or LXC containers on Proxmox VE, which can then serve as hosts for Docker or other services.
Each module should have its own `README.md` (eventually) detailing its purpose, inputs, and outputs.
## Future Plans
1. **Phase 1 (Current):** Codify all existing Dockerized services running on the primary Debian server ("casa") using OpenTofu.
2. **Phase 2:** Set up a new machine with Proxmox VE.
3. **Phase 3:** Adapt and expand these OpenTofu configurations to:
* Provision VMs on Proxmox (e.g., a dedicated Docker host VM).
* Deploy the Dockerized services (from Phase 1) inside these VMs using OpenTofu.
* Potentially manage LXC containers on Proxmox for suitable services.
---
Remember to replace placeholders like `<your-repo-url>` and customize the content based on the specifics of your setup as it evolves.

12
environments/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Homelab Environments
This directory contains environment-specific configurations that help organize your infrastructure modules into logical groupings.
Each subdirectory represents a category or environment that can be applied independently or together with others.
```
/environments/
├── core/ # Essential infrastructure (tunnel, monitoring)
├── services/ # Application services (ActualBudget, EmulatorJS)
└── network/ # (Future) Network configs
```

17
environments/core/main.tf Normal file
View File

@@ -0,0 +1,17 @@
// Core infrastructure components
// These are the foundational services that other services depend on
locals {
module_dir = "../../modules"
}
// Core monitoring and maintenance service
module "watchtower" {
source = "${local.module_dir}/20-services-apps/watchtower"
timezone = var.timezone
poll_interval = 86400
cleanup = true
enable_notifications = var.watchtower_enable_notifications
notification_url = var.watchtower_notification_url
}

View File

@@ -0,0 +1,19 @@
// Generic
variable "timezone" {
description = "Timezone for the system"
type = string
}
// Watchtower
variable "watchtower_enable_notifications" {
description = "Enable Watchtower update notifications"
type = bool
default = false
}
variable "watchtower_notification_url" {
description = "Webhook URL for Watchtower notifications (Discord, Slack, etc.)"
type = string
sensitive = true
default = ""
}

View File

@@ -0,0 +1,45 @@
// Network environment
// Contains configurations for network infrastructure
locals {
module_dir = "../../modules"
}
module "cloudflare_globals" {
source = "${local.module_dir}/00-globals/cloudflare"
cloudflare_api_token = var.cloudflare_api_token
cloudflare_account_id = var.cloudflare_account_id
cloudflare_zone_id = var.cloudflare_zone_id
domain = var.domain
}
module "homelab_docker_network" {
source = "${local.module_dir}/01-networking/docker-network"
name = "homelab-network"
driver = "bridge"
attachable = true
subnet = "10.100.0.0/16"
}
module "homelab_cloudflared_tunnel" {
source = "${local.module_dir}/01-networking/cloudflared-tunnel"
cloudflare_account_id = module.cloudflare_globals.cloudflare_account_id
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
tunnel_name = "homelab"
container_name = "cloudflared-homelab"
ingress_rules = [
{
hostname = "budget.${var.domain}"
service = "http://actualbudget:5006"
},
]
networks = [module.homelab_docker_network.name]
monitoring = true
}

View File

@@ -0,0 +1,36 @@
output "cloudflare_account_id" {
description = "Cloudflare account ID"
value = module.cloudflare_globals.cloudflare_account_id
}
output "cloudflare_zone_id" {
description = "Cloudflare zone ID"
value = module.cloudflare_globals.cloudflare_zone_id
}
output "domain" {
description = "Base domain name"
value = module.cloudflare_globals.domain
}
// Docker network outputs
output "homelab_docker_network_name" {
description = "Name of the Docker network"
value = module.homelab_docker_network.name
}
// Tunnel outputs
output "homelab_cloudflared_tunnel_id" {
description = "ID of the Cloudflare tunnel"
value = module.homelab_cloudflared_tunnel.tunnel_id
}
output "homelab_cloudflared_tunnel_name" {
description = "Name of the Cloudflare tunnel"
value = module.homelab_cloudflared_tunnel.tunnel_name
}
output "homelab_cloudflared_tunnel_cname_target" {
description = "CNAME target for the tunnel"
value = module.homelab_cloudflared_tunnel.cname_target
}

View File

@@ -0,0 +1,21 @@
variable "cloudflare_api_token" {
description = "API token for Cloudflare with the necessary permissions"
type = string
sensitive = true
}
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID for the domain"
type = string
}
variable "domain" {
description = "Base domain name (e.g., example.com)"
type = string
}

View File

@@ -0,0 +1,36 @@
// Application services environment
// Contains configurations for all application services
// Import global Terraform settings
terraform {
# Include backend configuration if needed
# backend "local" { ... }
}
locals {
module_dir = "../../modules"
}
module "actualbudget" {
source = "${local.module_dir}/20-services-apps/actualbudget"
container_name = "actualbudget"
timezone = var.timezone
data_volume_path = "${var.data_dir}/actual/data"
port = var.actualbudget_port
networks = var.default_networks
}
module "emulatorjs" {
source = "${local.module_dir}/20-services-apps/emulatorjs"
container_name = "emulatorjs"
timezone = var.timezone
puid = var.puid
pgid = var.pgid
config_volume_path = "${var.data_dir}/emulatorjs/config"
data_volume_path = "${var.data_dir}/emulatorjs/data"
frontend_port = var.emulatorjs_frontend_port
config_port = var.emulatorjs_config_port
backend_port = var.emulatorjs_backend_port
}

View File

@@ -0,0 +1,38 @@
// Services environment outputs
// ActualBudget
output "actualbudget_container_name" {
description = "The name of the ActualBudget container"
value = module.actualbudget.container_name
}
output "actualbudget_container_id" {
description = "The ID of the ActualBudget container"
value = module.actualbudget.container_id
}
output "actualbudget_local_url" {
description = "The local URL to access ActualBudget"
value = module.actualbudget.local_url
}
// EmulatorJS
output "emulatorjs_container_name" {
description = "The name of the EmulatorJS container"
value = module.emulatorjs.container_name
}
output "emulatorjs_container_id" {
description = "The ID of the EmulatorJS container"
value = module.emulatorjs.container_id
}
output "emulatorjs_frontend_url" {
description = "The frontend URL for EmulatorJS"
value = module.emulatorjs.frontend_url
}
output "emulatorjs_config_url" {
description = "The configuration URL for EmulatorJS"
value = module.emulatorjs.config_url
}

View File

@@ -0,0 +1,50 @@
// Variables for the services environment
// Generic
variable "timezone" {
description = "Timezone for the system"
type = string
}
variable "puid" {
description = "User ID for the container"
type = number
}
variable "pgid" {
description = "Group ID for the container"
type = number
}
variable "data_dir" {
description = "Base directory for data volumes"
type = string
}
variable "default_networks" {
description = "List of networks to which the container should be attached"
type = list(string)
default = []
}
// ActualBudget
variable "actualbudget_port" {
description = "External port for the ActualBudget server"
type = number
}
// EmulatorJS
variable "emulatorjs_frontend_port" {
description = "External port for the EmulatorJS frontend"
type = number
}
variable "emulatorjs_config_port" {
description = "External port for the EmulatorJS configuration interface"
type = number
}
variable "emulatorjs_backend_port" {
description = "External port for the EmulatorJS backend"
type = number
}

49
main.tf Normal file
View File

@@ -0,0 +1,49 @@
// Root module that orchestrates all environments
// This unified approach keeps a single entry point while organizing by function
// Network infrastructure
module "network" {
source = "./environments/network"
// Cloudflare variables
cloudflare_api_token = var.cloudflare_api_token
cloudflare_account_id = var.cloudflare_account_id
cloudflare_zone_id = var.cloudflare_zone_id
domain = var.domain
}
// Core infrastructure (monitoring, globals)
module "core" {
source = "./environments/core"
depends_on = [module.network]
timezone = var.timezone
// Watchtower variables
watchtower_enable_notifications = var.watchtower_enable_notifications
watchtower_notification_url = var.watchtower_notification_url
}
// Application services
module "services" {
source = "./environments/services"
depends_on = [module.core, module.network]
timezone = var.timezone
puid = var.puid
pgid = var.pgid
data_dir = var.data_dir
// ActualBudget variables
actualbudget_port = var.actualbudget_port
// EmulatorJS variables
emulatorjs_frontend_port = var.emulatorjs_frontend_port
emulatorjs_config_port = var.emulatorjs_config_port
emulatorjs_backend_port = var.emulatorjs_backend_port
// Docker network variables
default_networks = [module.network.homelab_docker_network_name]
}

View File

@@ -0,0 +1,20 @@
output "cloudflare_account_id" {
description = "Cloudflare account ID"
value = var.cloudflare_account_id
}
output "cloudflare_zone_id" {
description = "Cloudflare zone ID"
value = var.cloudflare_zone_id
}
output "domain" {
description = "Base domain name"
value = var.domain
}
output "cloudflare_api_token" {
description = "API token for Cloudflare"
value = var.cloudflare_api_token
sensitive = true
}

View File

@@ -0,0 +1,20 @@
variable "cloudflare_api_token" {
description = "API token for Cloudflare with tunnel, DNS, and zone management permissions"
type = string
sensitive = true
}
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID for your domain"
type = string
}
variable "domain" {
description = "Base domain name (e.g., example.com)"
type = string
}

View File

@@ -0,0 +1,123 @@
# Cloudflare Tunnel Module
This module creates and manages Cloudflare Tunnels using OpenTofu, automating the entire setup process including:
1. Creating the Cloudflare tunnel
2. Configuring tunnel routing rules
3. Setting up DNS records
4. Deploying the cloudflared tunnel container
## Features
- **Automated Tunnel Management**: Creates and configures Cloudflare tunnels via the API
- **Multiple Service Support**: Route multiple applications through a single tunnel
- **DNS Management**: Automatically creates DNS records for your applications
- **Docker Integration**: Deploys the cloudflared container with proper configuration
- **Secret Management**: Auto-generates tunnel secrets if not provided
## Prerequisites
Before using this module, you need:
1. A Cloudflare account
2. API token with the following permissions:
- Account.Cloudflare Tunnel:Edit
- Zone.DNS:Edit
- Zone.Zone:Read
3. Your Cloudflare account ID and zone ID
## Usage
```hcl
module "homelab_tunnel" {
source = "./modules/01-networking/cloudflared-tunnel"
cloudflare_account_id = var.cloudflare_account_id
cloudflare_zone_id = var.cloudflare_zone_id
tunnel_name = "homelab-tunnel"
container_name = "cloudflared-homelab"
ingress_rules = [
{
hostname = "budget.example.com"
service = "http://actualbudget:5006"
},
{
hostname = "dashboard.example.com"
service = "http://homepage:3000"
}
]
}
```
## Connecting with the Cloudflare Globals Module
For cleaner code organization, use the globals module:
```hcl
module "cloudflare_globals" {
source = "./modules/00-globals/cloudflare"
cloudflare_api_token = var.cloudflare_api_token
cloudflare_account_id = var.cloudflare_account_id
cloudflare_zone_id = var.cloudflare_zone_id
domain = "example.com"
}
module "homelab_tunnel" {
source = "./modules/01-networking/cloudflared-tunnel"
cloudflare_account_id = module.cloudflare_globals.cloudflare_account_id
cloudflare_zone_id = module.cloudflare_globals.cloudflare_zone_id
tunnel_name = "homelab-tunnel"
ingress_rules = [
{
hostname = "budget.${module.cloudflare_globals.domain}"
service = "http://actualbudget:5006"
}
]
}
```
## Variables
| Name | Description | Type | Default |
|------|-------------|------|---------|
| `cloudflare_account_id` | Cloudflare account ID | string | (required) |
| `cloudflare_zone_id` | Cloudflare zone ID for your domain | string | (required) |
| `container_name` | Name of the Cloudflare tunnel container | string | "" (defaults to "cloudflared-{tunnel_name}") |
| `image_tag` | Docker image tag for cloudflared | string | "latest" |
| `tunnel_name` | Name of the tunnel | string | (required) |
| `tunnel_secret` | Secret for the tunnel | string | "" (auto-generated if empty) |
| `ingress_rules` | List of ingress rules | list(object) | (required) |
| `monitoring` | Enable monitoring via Watchtower | bool | true |
### Ingress Rules Object Structure
```hcl
ingress_rules = [
{
hostname = "app.example.com" # FQDN for the service
service = "http://container:port" # Internal service URL
path = "/api/*" # Optional path pattern
create_dns_record = true # Whether to create DNS record (default: true)
}
]
```
## Outputs
| Name | Description |
|------|-------------|
| `tunnel_id` | ID of the created tunnel |
| `tunnel_name` | Name of the tunnel |
| `tunnel_token` | Token for the tunnel (sensitive) |
| `cname_target` | CNAME target for the tunnel |
| `dns_records` | Map of created DNS records |
| `container_name` | Name of the cloudflared container |
| `container_id` | ID of the cloudflared container |
| `image_id` | ID of the cloudflared image |
| `ip_address` | IP address of the cloudflared container |

View File

@@ -0,0 +1,95 @@
// Cloudflare Tunnel module
// This module creates a Cloudflare tunnel and deploys a cloudflared container
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.1"
}
}
}
// Generate a random secret for the tunnel if none provided
resource "random_id" "tunnel_secret" {
count = var.tunnel_secret == "" ? 1 : 0
byte_length = 35
}
// Create the Cloudflare Tunnel
resource "cloudflare_zero_trust_tunnel_cloudflared" "this" {
account_id = var.cloudflare_account_id
name = var.tunnel_name
secret = var.tunnel_secret != "" ? var.tunnel_secret : random_id.tunnel_secret[0].b64_std
}
locals {
all_ingress_rules = [for rule in var.ingress_rules : rule if rule != null]
}
// Configure tunnel routing
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "this" {
account_id = var.cloudflare_account_id
tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.this.id
config {
// Add all service ingress rules
dynamic "ingress_rule" {
for_each = local.all_ingress_rules
content {
hostname = ingress_rule.value.hostname
service = ingress_rule.value.service
}
}
// Default catch-all rule (required)
ingress_rule {
service = "http_status:404"
}
}
}
// Create DNS record for each service
resource "cloudflare_record" "service" {
for_each = { for rule in var.ingress_rules : rule.hostname => rule }
zone_id = var.cloudflare_zone_id
name = split(".", each.value.hostname)[0] // Extract subdomain
content = "${cloudflare_zero_trust_tunnel_cloudflared.this.id}.cfargotunnel.com"
type = "CNAME"
proxied = true
}
// Set up the Docker container
locals {
container_name = var.container_name != "" ? var.container_name : "cloudflared-${var.tunnel_name}"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
}
module "cloudflared" {
source = "../../10-services-generic/docker-service"
container_name = var.container_name
image = "cloudflare/cloudflared"
tag = local.image_tag
// Environment variables with tunnel token
env_vars = {
TUNNEL_TOKEN = cloudflare_zero_trust_tunnel_cloudflared.this.tunnel_token
}
// Command to run tunnel
command = ["tunnel", "--no-autoupdate", "run"]
// Restart policy
restart_policy = "unless-stopped"
// Enable monitoring for the container via Watchtower if specified
monitoring = var.monitoring
networks = var.networks
}

View File

@@ -0,0 +1,47 @@
// Outputs for the Cloudflare tunnel module
output "tunnel_id" {
description = "ID of the created Cloudflare tunnel"
value = cloudflare_zero_trust_tunnel_cloudflared.this.id
}
output "tunnel_name" {
description = "Name of the Cloudflare tunnel"
value = cloudflare_zero_trust_tunnel_cloudflared.this.name
}
output "tunnel_token" {
description = "Token for the Cloudflare tunnel"
value = cloudflare_zero_trust_tunnel_cloudflared.this.tunnel_token
sensitive = true
}
output "cname_target" {
description = "CNAME target for the tunnel"
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" {
description = "The name of the Cloudflared tunnel container"
value = module.cloudflared.container_name
}
output "container_id" {
description = "The ID of the Cloudflared tunnel container"
value = module.cloudflared.container_id
}
output "image_id" {
description = "The ID of the Cloudflared image"
value = module.cloudflared.image_id
}
output "ip_address" {
description = "The IP address of the Cloudflared container"
value = module.cloudflared.ip_address
}

View File

@@ -0,0 +1,57 @@
// Variables for the Cloudflare tunnel module
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID for your domain"
type = string
}
variable "container_name" {
description = "Name of the Cloudflare tunnel container"
type = string
default = ""
}
variable "image_tag" {
description = "Docker image tag for cloudflare/cloudflared"
type = string
default = "latest"
}
variable "tunnel_name" {
description = "Name of the Cloudflare tunnel"
type = string
}
variable "tunnel_secret" {
description = "Secret for the Cloudflare tunnel (will be auto-generated if empty)"
type = string
sensitive = true
default = ""
}
variable "ingress_rules" {
description = "List of ingress rules for services to be exposed through the tunnel"
type = list(object({
hostname = string
service = string
}))
default = []
}
variable "monitoring" {
description = "Enable monitoring via Watchtower"
type = bool
default = true
}
variable "networks" {
description = "List of networks to connect the container to"
type = list(string)
default = []
}

View File

@@ -0,0 +1,47 @@
# Docker Network Module
This module creates a Docker network that allows containers to communicate with each other using container names as hostnames.
## Purpose
The module is designed to create a consistent Docker network for all homelab services, enabling direct container-to-container communication using container names instead of IP addresses.
## Usage
```hcl
module "homelab_network" {
source = "../modules/01-networking/docker-network"
network_name = "homelab-network"
driver = "bridge"
# Optional: Configure specific subnet (uncomment if needed)
# subnet = "172.20.0.0/16"
# gateway = "172.20.0.1"
}
```
## Input Variables
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| `network_name` | Name of the Docker network | `string` | N/A | Yes |
| `driver` | Network driver to use | `string` | `"bridge"` | No |
| `internal` | Restrict external access if true | `bool` | `false` | No |
| `attachable` | Enable manual container attachment | `bool` | `true` | No |
| `ipam_driver` | IP address management driver | `string` | `"default"` | No |
| `subnet` | Subnet in CIDR format | `string` | `""` | No |
| `gateway` | Gateway IP for the subnet | `string` | `""` | No |
| `ip_range` | Range for container IP allocation | `string` | `""` | No |
| `aux_address` | Auxiliary addresses for driver | `map(string)` | `{}` | No |
| `labels` | Docker labels to add to the network | `map(string)` | `{}` | No |
| `options` | Driver-specific options | `map(string)` | `{}` | No |
## Outputs
| Name | Description |
|------|-------------|
| `network_id` | The ID of the created Docker network |
| `network_name` | The name of the Docker network |
| `network_driver` | The driver of the Docker network |
| `ipam_config` | The IPAM configuration of the network |

View File

@@ -0,0 +1,31 @@
// Docker Network Module
// This module creates a Docker network for container communication
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.6.0"
}
}
}
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
}

View File

@@ -0,0 +1,21 @@
// Outputs for Docker Network module
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
}

View File

@@ -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 = {}
}

View File

@@ -0,0 +1,89 @@
# 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"
}
// Enable Watchtower updates
monitoring = true
}
```
## Required Providers
This module requires the Docker provider to be configured in your root module:
```hcl
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = ">= 3.0.0"
}
}
}
```
## 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 |

View File

@@ -0,0 +1,133 @@
// Generic Docker service module
// Creates and manages a Docker container with configurable options
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = ">= 3.0.0"
}
}
}
locals {
network_mode = var.network_mode
container_name = var.container_name
image_name = "${var.image}:${var.tag}"
// 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
}
]
// Define monitoring labels if enabled
monitoring_labels = var.monitoring ? {
"com.centurylinklabs.watchtower.enable" = "true"
} : {}
// Merge provided labels with monitoring labels
merged_labels = merge(var.labels, local.monitoring_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
# 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 var.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
}

View File

@@ -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
}

View File

@@ -0,0 +1,181 @@
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 = "unless-stopped"
}
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 "labels" {
description = "Docker container labels"
type = map(string)
default = {}
}
variable "monitoring" {
description = "Enable container monitoring via Watchtower"
type = bool
default = true
}
variable "healthcheck" {
description = "Container healthcheck configuration"
type = object({
test = list(string)
interval = string
timeout = string
start_period = 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"
}
}

View File

@@ -0,0 +1,49 @@
// ActualBudget module for budgeting
// This module configures an ActualBudget container with the specified volumes
locals {
container_name = var.container_name != "" ? var.container_name : "actualbudget"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
default_env_vars = {
TZ = var.timezone
PUID = var.puid
PGID = var.pgid
}
default_volumes = [
{
container_path = "/data"
host_path = var.data_volume_path
read_only = false
}
]
}
module "actualbudget" {
source = "../../10-services-generic/docker-service"
container_name = var.container_name
image = "actualbudget/actual-server"
tag = var.image_tag
// Environment variables
env_vars = local.default_env_vars
// Port mapping
ports = [
{
internal = 5006
external = var.port
protocol = "tcp"
}
]
// Volume mapping
volumes = local.default_volumes
// Enable monitoring for the container via Watchtower
monitoring = var.monitoring
networks = var.networks
}

View File

@@ -0,0 +1,24 @@
output "container_name" {
description = "The name of the ActualBudget container"
value = module.actualbudget.container_name
}
output "container_id" {
description = "The ID of the ActualBudget container"
value = module.actualbudget.container_id
}
output "image_id" {
description = "The ID of the ActualBudget image"
value = module.actualbudget.image_id
}
output "ip_address" {
description = "The IP address of the ActualBudget container"
value = module.actualbudget.ip_address
}
output "local_url" {
description = "The local URL to access the ActualBudget interface"
value = "http://localhost:${var.port}"
}

View File

@@ -0,0 +1,52 @@
variable "container_name" {
description = "Name of the ActualBudget container"
type = string
default = "actualbudget"
}
variable "timezone" {
description = "Timezone for the container"
type = string
default = "UTC"
}
variable "image_tag" {
description = "Tag of the ActualBudget image to use"
type = string
default = "latest"
}
variable "port" {
description = "External port for ActualBudget server"
type = number
default = 5006
}
variable "data_volume_path" {
description = "Host path for ActualBudget data volume"
type = string
}
variable "puid" {
description = "User ID for the container"
type = number
default = 1000
}
variable "pgid" {
description = "Group ID for the container"
type = number
default = 1000
}
variable "monitoring" {
description = "Enable monitoring for the container via Watchtower"
type = bool
default = true
}
variable "networks" {
description = "List of networks to which the container should be attached"
type = list(string)
default = []
}

View File

@@ -0,0 +1,70 @@
// EmulatorJS module for retro game emulation
// This module configures an EmulatorJS container with the specified volumes
locals {
container_name = var.container_name != "" ? var.container_name : "emulatorjs"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
default_env_vars = {
TZ = var.timezone
}
// Merge default env vars with any additional ones provided
env_vars = merge(local.default_env_vars, var.additional_env_vars)
// Default volumes for EmulatorJS
default_volumes = [
{
host_path = var.config_volume_path
container_path = "/config"
read_only = false
},
{
host_path = var.data_volume_path
container_path = "/data"
read_only = false
}
]
// Merge default volumes with any additional ones provided
volumes = concat(local.default_volumes, var.additional_volumes)
}
// Use the generic docker-service module to deploy EmulatorJS
module "emulatorjs" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = "linuxserver/emulatorjs"
tag = local.image_tag
restart_policy = var.restart_policy
network_mode = "bridge"
env_vars = local.env_vars
volumes = local.volumes
labels = var.labels
// Default ports for EmulatorJS
ports = [
{
internal = 3000
external = var.config_port
protocol = "tcp"
},
{
internal = 80
external = var.frontend_port
protocol = "tcp"
},
{
internal = 4001
external = var.backend_port
protocol = "tcp"
}
]
// Enable monitoring for the container via Watchtower
monitoring = var.monitoring
}

View File

@@ -0,0 +1,26 @@
// Outputs for the EmulatorJS module
output "container_name" {
description = "Name of the created EmulatorJS container"
value = module.emulatorjs.container_name
}
output "container_id" {
description = "ID of the created EmulatorJS container"
value = module.emulatorjs.container_id
}
output "image_id" {
description = "ID of the EmulatorJS image used"
value = module.emulatorjs.image_id
}
output "frontend_url" {
description = "URL to access the EmulatorJS frontend interface"
value = "http://localhost:${var.frontend_port}"
}
output "config_url" {
description = "URL to access the EmulatorJS web configuration interface"
value = "http://localhost:${var.config_port}"
}

View File

@@ -0,0 +1,91 @@
variable "container_name" {
description = "Name for the EmulatorJS container"
type = string
default = "emulatorjs"
}
variable "image_tag" {
description = "The tag for the EmulatorJS container image"
type = string
default = "latest"
}
variable "restart_policy" {
description = "Restart policy for the container"
type = string
default = "unless-stopped"
}
variable "timezone" {
description = "Timezone for the container"
type = string
default = "Etc/UTC"
}
variable "puid" {
description = "User ID the container will run as"
type = number
default = 1000
}
variable "pgid" {
description = "Group ID the container will run as"
type = number
default = 1000
}
variable "config_volume_path" {
description = "Host path for the EmulatorJS config directory"
type = string
}
variable "data_volume_path" {
description = "Host path for the EmulatorJS data directory"
type = string
}
variable "frontend_port" {
description = "External port for the EmulatorJS frontend"
type = number
default = 3000
}
variable "config_port" {
description = "External port for the EmulatorJS configuration interface"
type = number
default = 8080
}
variable "backend_port" {
description = "External port for the EmulatorJS backend"
type = number
default = 4001
}
variable "additional_env_vars" {
description = "Additional environment variables for EmulatorJS"
type = map(string)
default = {}
}
variable "additional_volumes" {
description = "Additional volumes to mount in the container"
type = list(object({
host_path = string
container_path = string
read_only = bool
}))
default = []
}
variable "labels" {
description = "Labels to set on the container"
type = map(string)
default = {}
}
variable "monitoring" {
description = "Enable monitoring for the container via Watchtower"
type = bool
default = true
}

View File

@@ -0,0 +1,79 @@
# Watchtower Module
This module deploys a Watchtower container which automatically updates your running Docker containers when new images become available.
## Features
- Automatic updates for Docker containers
- Configurable update schedule
- Optional cleanup of old images
- Notification support via shoutrrr
- Container monitoring options
## Usage
To use this module in your root module, add the following code:
```hcl
module "watchtower" {
source = "./modules/20-services-apps/watchtower"
# Basic configuration
container_name = "watchtower"
image_tag = "latest"
timezone = "Australia/Sydney"
# Update settings
poll_interval = 86400 # Check once per day (in seconds)
cleanup = true # Remove old images after updating
rolling_restart = true # Update containers one by one
# Optional notification settings
enable_notifications = false
# notification_url = "discord://webhook-id/webhook-token"
# Additional settings as needed
# additional_env_vars = {
# WATCHTOWER_MONITOR_ONLY = "true"
# }
}
```
## Required Resources
This module leverages the generic `docker-service` module, which handles the Docker container deployment.
## Input Variables
| Name | Description | Type | Default |
|------|-------------|------|---------|
| container_name | Name for the Watchtower container | string | "watchtower" |
| image_tag | The tag for the Watchtower container image | string | "latest" |
| restart_policy | Restart policy for the container | string | "unless-stopped" |
| timezone | Timezone for the container | string | "Etc/UTC" |
| cleanup | Remove old images after updating | bool | true |
| poll_interval | Poll interval (in seconds) for checking updates | number | 86400 |
| include_stopped | Include stopped containers when checking for updates | bool | false |
| revive_stopped | Restart stopped containers after updating | bool | false |
| rolling_restart | Restart containers one by one instead of all at once | bool | true |
| notification_url | URL for sending update notifications via shoutrrr | string | "" |
| enable_notifications | Enable shoutrrr notifications | bool | false |
| additional_env_vars | Additional environment variables for Watchtower | map(string) | {} |
| additional_volumes | Additional volumes to mount in the container | list(object) | [] |
| labels | Labels to set on the container | map(string) | {} |
| ports | Ports to expose (rarely needed for Watchtower) | list(object) | [] |
| monitoring | Enable monitoring for the container | bool | true |
## Outputs
| Name | Description |
|------|-------------|
| container_name | Name of the created Watchtower container |
| container_id | ID of the created Watchtower container |
| image_id | ID of the Watchtower image used |
## Notes
- Watchtower needs access to the Docker socket to monitor and update containers
- For security-conscious environments, consider limiting which containers Watchtower can update
- See the [Watchtower documentation](https://containrrr.dev/watchtower/) for more advanced configuration options

View File

@@ -0,0 +1,58 @@
// Watchtower module for automatic Docker container updates
// This module configures a Watchtower container that monitors and updates other containers
locals {
container_name = var.container_name != "" ? var.container_name : "watchtower"
image_tag = var.image_tag != "" ? var.image_tag : "latest"
default_env_vars = {
TZ = var.timezone
WATCHTOWER_CLEANUP = var.cleanup
WATCHTOWER_POLL_INTERVAL = var.poll_interval
WATCHTOWER_INCLUDE_STOPPED = var.include_stopped
WATCHTOWER_REVIVE_STOPPED = var.revive_stopped
WATCHTOWER_ROLLING_RESTART = var.rolling_restart
WATCHTOWER_NOTIFICATION_URL = var.notification_url
WATCHTOWER_NOTIFICATIONS = var.enable_notifications ? "shoutrrr" : ""
}
// Merge default env vars with any additional ones provided
env_vars = merge(local.default_env_vars, var.additional_env_vars)
// Default volumes for Docker socket access
default_volumes = [
{
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
read_only = true
}
]
// Merge default volumes with any additional ones provided
volumes = concat(local.default_volumes, var.additional_volumes)
}
// Use the generic docker-service module to deploy Watchtower
module "watchtower" {
source = "../../10-services-generic/docker-service"
container_name = local.container_name
image = "containrrr/watchtower"
tag = local.image_tag
restart_policy = var.restart_policy
network_mode = "bridge"
env_vars = local.env_vars
volumes = local.volumes
labels = var.labels
// Watchtower doesn't typically expose ports but we'll include the option
ports = var.ports
// Add monitoring label if enabled
monitoring = var.monitoring
depends_on = []
}

View File

@@ -0,0 +1,14 @@
output "container_name" {
description = "Name of the created Watchtower container"
value = module.watchtower.container_name
}
output "container_id" {
description = "ID of the created Watchtower container"
value = module.watchtower.container_id
}
output "image_id" {
description = "ID of the Watchtower image used"
value = module.watchtower.image_id
}

View File

@@ -0,0 +1,103 @@
variable "container_name" {
description = "Name for the Watchtower container"
type = string
default = "watchtower"
}
variable "image_tag" {
description = "The tag for the Watchtower container image"
type = string
default = "latest"
}
variable "restart_policy" {
description = "Restart policy for the container"
type = string
default = "unless-stopped"
}
variable "timezone" {
description = "Timezone for the container"
type = string
default = "Etc/UTC"
}
variable "cleanup" {
description = "Remove old images after updating"
type = bool
default = true
}
variable "poll_interval" {
description = "Poll interval (in seconds) for checking for updates"
type = number
default = 86400 // Default: check once per day
}
variable "include_stopped" {
description = "Include stopped containers when checking for updates"
type = bool
default = false
}
variable "revive_stopped" {
description = "Restart stopped containers after updating"
type = bool
default = false
}
variable "rolling_restart" {
description = "Restart containers one by one instead of all at once"
type = bool
default = true
}
variable "notification_url" {
description = "URL for sending update notifications via shoutrrr"
type = string
default = ""
}
variable "enable_notifications" {
description = "Enable shoutrrr notifications"
type = bool
default = false
}
variable "additional_env_vars" {
description = "Additional environment variables for Watchtower"
type = map(string)
default = {}
}
variable "additional_volumes" {
description = "Additional volumes to mount in the container"
type = list(object({
host_path = string
container_path = string
read_only = bool
}))
default = []
}
variable "labels" {
description = "Labels to set on the container"
type = map(string)
default = {}
}
variable "ports" {
description = "Ports to expose (Watchtower typically doesn't need ports exposed)"
type = list(object({
internal = number
external = number
protocol = string
}))
default = []
}
variable "monitoring" {
description = "Enable monitoring for the container"
type = bool
default = true
}

33
outputs.tf Normal file
View File

@@ -0,0 +1,33 @@
// Root outputs that expose important information from each environment
// Network environment outputs
output "cloudflare_domain" {
description = "Base domain for the homelab"
value = module.network.domain
}
output "homelab_cloudflared_tunnel_name" {
description = "Name of the Cloudflare tunnel"
value = module.network.homelab_cloudflared_tunnel_name
}
output "homelab_cloudflared_tunnel_cname_target" {
description = "CNAME target for the Cloudflare tunnel"
value = module.network.homelab_cloudflared_tunnel_cname_target
}
// Service URLs
output "actualbudget_local_url" {
description = "Local URL for accessing ActualBudget"
value = module.services.actualbudget_local_url
}
output "emulatorjs_frontend_url" {
description = "URL for the EmulatorJS frontend"
value = module.services.emulatorjs_frontend_url
}
output "emulatorjs_config_url" {
description = "URL for the EmulatorJS configuration"
value = module.services.emulatorjs_config_url
}

20
providers.tf Normal file
View File

@@ -0,0 +1,20 @@
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.6.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.1"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}

26
terraform.tfvars.example Normal file
View File

@@ -0,0 +1,26 @@
# Example terraform.tfvars file
# Copy to terraform.tfvars and fill with your values
# Generic
timezone = "Australia/Sydney"
puid = 1000
pgid = 1000
data_dir = "/srv/docker_data"
# Watchtower
watchtower_enable_notifications = false
# watchtower_notification_url = "discord://token@webhookId"
# EmulatorJS
emulatorjs_frontend_port = 5823
emulatorjs_config_port = 5824
emulatorjs_backend_port = 5825
# ActualBudget
actualbudget_port = 5006
# Cloudflare
cloudflare_api_token = "your-cloudflare-api-token" # API token with required permissions
cloudflare_account_id = "your-cloudflare-account-id" # Found in Cloudflare dashboard URL
cloudflare_zone_id = "your-cloudflare-zone-id" # Found in the domain overview page
domain = "yourdomain.com" # Your domain on Cloudflare

79
variables.tf Normal file
View File

@@ -0,0 +1,79 @@
// Generic
variable "timezone" {
description = "Timezone for the system"
type = string
}
variable "puid" {
description = "User ID for the container"
type = number
}
variable "pgid" {
description = "Group ID for the container"
type = number
}
variable "data_dir" {
description = "Base directory for data volumes"
type = string
}
// Watchtower
variable "watchtower_enable_notifications" {
description = "Enable Watchtower update notifications"
type = bool
default = false
}
variable "watchtower_notification_url" {
description = "Webhook URL for Watchtower notifications (Discord, Slack, etc.)"
type = string
sensitive = true // This flags the variable as sensitive in logs and outputs
default = ""
}
// EmulatorJS
variable "emulatorjs_frontend_port" {
description = "External port for the EmulatorJS frontend"
type = number
}
variable "emulatorjs_config_port" {
description = "External port for the EmulatorJS configuration interface"
type = number
}
variable "emulatorjs_backend_port" {
description = "External port for the EmulatorJS backend"
type = number
}
// ActualBudget
variable "actualbudget_port" {
description = "External port for the ActualBudget server"
type = number
}
// Cloudflare
variable "cloudflare_api_token" {
description = "API token for Cloudflare with tunnel, DNS, and zone management permissions"
type = string
sensitive = true
}
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID for your domain"
type = string
}
variable "domain" {
description = "Base domain name (e.g., example.com)"
type = string
}

0
versions.tf Normal file
View File