diff --git a/modules/20-services-apps/immich/.env.example b/modules/20-services-apps/immich/.env.example new file mode 100644 index 0000000..be7eca3 --- /dev/null +++ b/modules/20-services-apps/immich/.env.example @@ -0,0 +1,14 @@ +# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables + +################################################################################### +# Required database configuration (used by Terraform to configure Postgres & Immich) +################################################################################### + +# PostgreSQL username +DB_USERNAME=postgres + +# PostgreSQL password (use only A-Za-z0-9 characters) +DB_PASSWORD=postgres + +# PostgreSQL database name +DB_DATABASE_NAME=immich diff --git a/modules/20-services-apps/immich/README.md b/modules/20-services-apps/immich/README.md new file mode 100644 index 0000000..878d099 --- /dev/null +++ b/modules/20-services-apps/immich/README.md @@ -0,0 +1,110 @@ +# Immich Module + +This module deploys [Immich](https://immich.app/), a high-performance self-hosted photo and video backup solution, as Docker containers in the homelab environment. + +## Overview + +The Immich module: + +- Deploys four Docker containers: + - `immich-server`: The main Immich API/UI server (port 2283) + - `immich-machine-learning`: The ML service for search, faces, and embeddings + - `immich-postgres`: Immich-tuned PostgreSQL database + - `immich-redis`: Valkey/Redis-compatible cache +- Creates a dedicated Docker network (`immich-network`) for inter-container communication +- Persists data to volumes on the host +- Provides a service definition for integration with networking modules + +## Usage + +```hcl +module "immich" { + source = "./modules/20-services-apps/immich" + appdata_path = "/path/to/appdata/immich" + library_path = "/path/to/data/media/photos" + networks = ["homelab-network"] +} +``` + +## Variables + +| Variable | Description | Type | Default | +| --------------- | -------------------------------------------------------------------------------- | -------------- | ---------- | +| `image_tag` | Tag of the Immich images to use (`server` and `machine-learning`) | `string` | `"release"` | +| `appdata_path` | Base host path for Immich app data (e.g., PostgreSQL data and internal configs) | `string` | - | +| `library_path` | Base host path for user library data and ML cache | `string` | - | +| `networks` | List of additional networks to which the server should attach | `list(string)` | `[]` | + +## Outputs + +| Output | Description | +| -------------------- | ---------------------------------------------------------- | +| `service_definition` | Service definition for integration with networking modules | + +## Service Definition + +This module outputs a service definition used by networking modules to expose the service. + +```hcl +{ + name = "immich-server" + primary_port = 2283 + endpoint = "http://immich-server:2283" + subdomains = ["photos"] + publish_via = "reverse_proxy" +} +``` + +## Environment Variables + +Only the database credentials are expected in a `.env` file in this module directory and are read using the `dotenv` Terraform provider. Everything else is configured directly in Terraform. + +Required in `modules/20-services-apps/immich/.env`: + +- `DB_USERNAME`: PostgreSQL user +- `DB_PASSWORD`: PostgreSQL password +- `DB_DATABASE_NAME`: Database name + +A ready-to-copy `modules/20-services-apps/immich/.env.example` is provided. + +## Data Persistence + +Immich stores data in the following volumes: + +1. Library storage: `/data` in `immich-server`, mapped to `${library_path}/library` on the host +2. ML model cache: `/cache` in `immich-machine-learning`, mapped to `${library_path}/machine-learning/cache` on the host +3. PostgreSQL data: `/var/lib/postgresql/data` in `immich-postgres`, mapped to `${appdata_path}/postgres/pgdata` on the host + +## Networking + +The module creates a dedicated Docker network named `immich-network` for communication between Immich components. The Immich server container is also attached to any additional networks specified in the `networks` variable, allowing it to communicate with other services in the homelab. + +## Dependencies + +- `immich-server` depends on `immich-postgres` and `immich-redis` +- `immich-postgres` and `immich-redis` include healthchecks +- The ML service is independent and discovered by the server internally; tuning can be done via the Immich admin UI + +## Integration with Networking Modules + +This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`. + +## Example Integration in Main Configuration + +```hcl +module "immich" { + source = "./modules/20-services-apps/immich" + appdata_path = "${module.system_globals.volume_host}/appdata/immich" + library_path = "${module.system_globals.volume_host}/data/media/photos" + networks = [module.services.homelab_docker_network_name] +} + +# The service definition is automatically included in the services output +module "services" { + source = "./modules/services" + # ... + service_definitions = [ + module.immich.service_definition, + # Other service definitions + ] +} diff --git a/modules/20-services-apps/immich/main.tf b/modules/20-services-apps/immich/main.tf new file mode 100644 index 0000000..4aa8f47 --- /dev/null +++ b/modules/20-services-apps/immich/main.tf @@ -0,0 +1,197 @@ +terraform { + required_providers { + dotenv = { + source = "germanbrew/dotenv" + } + } +} + +variable "image_tag" { + description = "The tag for the Immich container images (server and machine-learning)" + type = string + default = "release" +} + +variable "appdata_path" { + description = "Base directory for Immich app data" + type = string +} + +variable "library_path" { + description = "Base directory for Immich library data" + type = string +} + +variable "networks" { + description = "List of networks to which the Immich server should be attached (in addition to the module network)" + type = list(string) + default = [] +} + +locals { + env_file = "${path.module}/.env" + monitoring = true + + # Container names + server_name = "immich-server" + ml_name = "immich-machine-learning" + redis_name = "immich-redis" + postgres_name = "immich-postgres" + + # Images and tags + server_image = "ghcr.io/immich-app/immich-server" + ml_image = "ghcr.io/immich-app/immich-machine-learning" + redis_image = "docker.io/valkey/valkey" + postgres_image = "ghcr.io/immich-app/postgres" + + server_tag = var.image_tag + ml_tag = var.image_tag + redis_tag = "8-bookworm" + postgres_tag = "14-vectorchord0.4.3-pgvectors0.2.0" + + # Ports + server_port = 2283 + ml_port = 3003 + + # Volumes (host paths) + server_volumes = [ + { + host_path = "${var.library_path}/data" + container_path = "/data" + read_only = false + } + ] + + ml_volumes = [ + { + host_path = "${var.library_path}/ml/cache" + container_path = "/cache" + read_only = false + } + ] + + postgres_volumes = [ + { + host_path = "${var.appdata_path}/postgres/pgdata" + container_path = "/var/lib/postgresql/data" + read_only = false + } + ] + + # Environment variables for Postgres + postgres_env_vars = { + POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file) + POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file) + POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file) + POSTGRES_INITDB_ARGS = "--data-checksums" + } + + # Environment variables for Immich server + server_env_vars = { + # Database + DB_HOSTNAME = local.postgres_name + DB_PORT = "5432" + DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file) + DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file) + DB_DATABASE_NAME = provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file) + + # Redis + REDIS_HOSTNAME = local.redis_name + REDIS_PORT = "6379" + REDIS_DBINDEX = "0" + + # General + IMMICH_MEDIA_LOCATION = "/data" + } + + # Healthchecks + redis_healthcheck = { + test = ["CMD", "redis-cli", "ping"] + interval = "10s" + timeout = "5s" + retries = 5 + start_period = "5s" + } + + postgres_healthcheck = { + test = ["CMD", "pg_isready", "-U", provider::dotenv::get_by_key("DB_USERNAME", local.env_file), "-d", provider::dotenv::get_by_key("DB_DATABASE_NAME", local.env_file)] + interval = "10s" + timeout = "5s" + retries = 5 + start_period = "5s" + } +} + +# Dedicated network for Immich +module "immich_network" { + source = "../../01-networking/docker-network" + name = "immich-network" + driver = "bridge" +} + +# Valkey (Redis) service +module "redis" { + source = "../../10-services-generic/docker-service" + container_name = local.redis_name + image = local.redis_image + tag = local.redis_tag + networks = [module.immich_network.name] + monitoring = local.monitoring + healthcheck = local.redis_healthcheck +} + +# Postgres service (Immich custom image) +module "postgres" { + source = "../../10-services-generic/docker-service" + container_name = local.postgres_name + image = local.postgres_image + tag = local.postgres_tag + volumes = local.postgres_volumes + env_vars = local.postgres_env_vars + networks = [module.immich_network.name] + monitoring = local.monitoring + healthcheck = local.postgres_healthcheck +} + +# Immich Machine Learning service +module "machine_learning" { + source = "../../10-services-generic/docker-service" + container_name = local.ml_name + image = local.ml_image + tag = local.ml_tag + volumes = local.ml_volumes + networks = [module.immich_network.name] + monitoring = local.monitoring +} + +# Immich Server service +module "immich" { + source = "../../10-services-generic/docker-service" + container_name = local.server_name + image = local.server_image + tag = local.server_tag + ports = [ + { + internal = local.server_port + external = local.server_port + protocol = "tcp" + } + ] + volumes = local.server_volumes + env_vars = local.server_env_vars + networks = concat([module.immich_network.name], var.networks) + monitoring = local.monitoring + depends_on = [module.postgres, module.redis] +} + +output "service_definition" { + description = "General service definition with optional ingress configuration" + value = { + name = local.server_name + primary_port = local.server_port + endpoint = "http://${local.server_name}:${local.server_port}" + subdomains = ["photos"] + publish_via = "reverse_proxy" + proxied = false + } +} diff --git a/services/main.tf b/services/main.tf index 93d6871..36002fa 100755 --- a/services/main.tf +++ b/services/main.tf @@ -2,6 +2,7 @@ locals { module_dir = "../modules" root_volume = module.system_globals.volume_host volume_host = "${module.system_globals.volume_host}/appdata" + data_host = "${module.system_globals.volume_host}/data" } module "system_globals" { @@ -62,6 +63,13 @@ module "glance" { networks = [module.homelab_docker_network.name] } +module "immich" { + source = "${local.module_dir}/20-services-apps/immich" + appdata_path = "${local.volume_host}/immich" + library_path = "${local.data_host}/media/photos" + networks = [module.homelab_docker_network.name] +} + module "linkwarden" { source = "${local.module_dir}/20-services-apps/linkwarden" volume_path = "${local.volume_host}/linkwarden" diff --git a/services/outputs.tf b/services/outputs.tf index 82f09ef..579e037 100755 --- a/services/outputs.tf +++ b/services/outputs.tf @@ -11,6 +11,7 @@ output "service_definitions" { module.crawl4ai.service_definition, module.emulatorjs.service_definition, module.glance.service_definition, + module.immich.service_definition, module.linkwarden.service_definition, module.n8n.service_definition, module.n8n.n8n_mcp_service_definition,