From ce8f81db0cd1c6183a2fd2181f379c9db78ab0eb Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 8 Feb 2026 02:18:14 +0000 Subject: [PATCH] [ci skip] Deploy Gramps Web genealogy service Add grampsweb module with web app + Celery worker in a single pod, using shared Redis (DB 2/3), NFS storage, email via mailserver, and Ollama AI integration. Available at family.viktorbarzin.me. --- .claude/CLAUDE.md | 22 ++- modules/kubernetes/grampsweb/main.tf | 266 +++++++++++++++++++++++++++ modules/kubernetes/main.tf | 12 +- 3 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 modules/kubernetes/grampsweb/main.tf diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e3ae8ebf..433d8e3a 100755 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -151,6 +151,7 @@ When configuring services to use the mailserver: - AFFiNE: stable (visual canvas, uses PostgreSQL + Redis) - Wyoming Whisper: latest (STT for Home Assistant, CPU on GPU node) - Health: latest (Apple Health data dashboard, Svelte + FastAPI + Caddy, uses PostgreSQL) +- Gramps Web: latest (genealogy, uses Redis + Celery) ## Useful Commands ```bash @@ -278,6 +279,7 @@ Top-level modules in `main.tf`: | affine | Visual canvas/whiteboard (PostgreSQL + Redis) | aux | | health | Apple Health data dashboard (PostgreSQL) | aux | | whisper | Wyoming Faster Whisper STT (CPU on GPU node) | gpu | +| grampsweb | Genealogy web app (Gramps Web) | aux | --- @@ -299,7 +301,7 @@ owntracks, dawarich, tuya, meshcentral, nextcloud, actualbudget, onlyoffice, forgejo, freshrss, navidrome, ollama, openwebui, isponsorblocktv, speedtest, freedify, rybbit, paperless, servarr, prowlarr, bazarr, radarr, sonarr, flaresolverr, -jellyfin, jellyseerr, tdarr, affine, health +jellyfin, jellyseerr, tdarr, affine, health, family ``` ### Special Subdomains @@ -456,3 +458,21 @@ Skills are specialized workflows for common tasks. Located in `.claude/skills/`. - **Access**: `10.0.20.202:10300` (Traefik LB IP, no public DNS) - **HA Integration**: Wyoming Protocol integration in ha-london, host `10.0.20.202`, port `10300` - **No GPU acceleration**: Official image is CPU-only (Debian + PyTorch CPU). The `mib1185/wyoming-faster-whisper-cuda` image exists but requires self-build. + +### Gramps Web (Genealogy) +- **Image**: `ghcr.io/gramps-project/grampsweb:latest` +- **Port**: 5000 +- **URL**: `https://family.viktorbarzin.me` +- **Components**: Web app + Celery worker (2 containers in 1 pod) +- **Requires**: Shared Redis (DB 2 for Celery broker/backend, DB 3 for rate limiting) +- **Storage**: NFS at `/mnt/main/grampsweb` with sub_paths: users, indexdir, thumbnail_cache, cache, secret, grampsdb, media, tmp +- **Key env vars**: + - `GRAMPSWEB_SECRET_KEY` - Flask secret key (generated via `random_password`) + - `GRAMPSWEB_TREE` - Tree name + - `GRAMPSWEB_BASE_URL` - Public URL + - `GRAMPSWEB_CELERY_CONFIG__broker_url` / `result_backend` - Redis connection + - `GRAMPSWEB_REGISTRATION_DISABLED` - Set to `True` + - `GRAMPSWEB_EMAIL_*` - SMTP configuration + - `GRAMPSWEB_LLM_*` - Ollama AI integration +- **Celery command**: `celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=2` +- **Registration**: Disabled; first user created via UI setup wizard diff --git a/modules/kubernetes/grampsweb/main.tf b/modules/kubernetes/grampsweb/main.tf new file mode 100644 index 00000000..d963b1b1 --- /dev/null +++ b/modules/kubernetes/grampsweb/main.tf @@ -0,0 +1,266 @@ +variable "tls_secret_name" {} +variable "tier" { type = string } +variable "smtp_password" { type = string } + +resource "kubernetes_namespace" "grampsweb" { + metadata { + name = "grampsweb" + } +} + +module "tls_secret" { + source = "../setup_tls_secret" + namespace = kubernetes_namespace.grampsweb.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +resource "random_password" "secret_key" { + length = 64 + special = false +} + +locals { + common_env = [ + { + name = "GRAMPSWEB_TREE" + value = "Gramps Web" + }, + { + name = "GRAMPSWEB_SECRET_KEY" + value = random_password.secret_key.result + }, + { + name = "GRAMPSWEB_CELERY_CONFIG__broker_url" + value = "redis://redis.redis.svc.cluster.local:6379/2" + }, + { + name = "GRAMPSWEB_CELERY_CONFIG__result_backend" + value = "redis://redis.redis.svc.cluster.local:6379/2" + }, + { + name = "GRAMPSWEB_RATELIMIT_STORAGE_URI" + value = "redis://redis.redis.svc.cluster.local:6379/3" + }, + { + name = "GRAMPSWEB_BASE_URL" + value = "https://family.viktorbarzin.me" + }, + { + name = "GRAMPSWEB_REGISTRATION_DISABLED" + value = "True" + }, + { + name = "GRAMPSWEB_EMAIL_HOST" + value = "mail.viktorbarzin.me" + }, + { + name = "GRAMPSWEB_EMAIL_PORT" + value = "587" + }, + { + name = "GRAMPSWEB_EMAIL_HOST_USER" + value = "info@viktorbarzin.me" + }, + { + name = "GRAMPSWEB_EMAIL_HOST_PASSWORD" + value = var.smtp_password + }, + { + name = "GRAMPSWEB_EMAIL_USE_SSL" + value = "False" + }, + { + name = "GRAMPSWEB_EMAIL_USE_STARTTLS" + value = "True" + }, + { + name = "GRAMPSWEB_DEFAULT_FROM_EMAIL" + value = "info@viktorbarzin.me" + }, + { + name = "GRAMPSWEB_LLM_BASE_URL" + value = "http://ollama.ollama.svc.cluster.local:11434/v1" + }, + { + name = "GRAMPSWEB_LLM_MODEL" + value = "llama3.1" + }, + ] +} + +resource "kubernetes_deployment" "grampsweb" { + metadata { + name = "grampsweb" + namespace = kubernetes_namespace.grampsweb.metadata[0].name + labels = { + app = "grampsweb" + tier = var.tier + } + } + spec { + replicas = 1 + selector { + match_labels = { + app = "grampsweb" + } + } + template { + metadata { + labels = { + app = "grampsweb" + } + } + spec { + container { + name = "grampsweb" + image = "ghcr.io/gramps-project/grampsweb:latest" + + port { + container_port = 5000 + } + + dynamic "env" { + for_each = local.common_env + content { + name = env.value.name + value = env.value.value + } + } + + volume_mount { + name = "data" + mount_path = "/app/users" + sub_path = "users" + } + volume_mount { + name = "data" + mount_path = "/app/indexdir" + sub_path = "indexdir" + } + volume_mount { + name = "data" + mount_path = "/app/thumbnail_cache" + sub_path = "thumbnail_cache" + } + volume_mount { + name = "data" + mount_path = "/app/cache" + sub_path = "cache" + } + volume_mount { + name = "data" + mount_path = "/app/secret" + sub_path = "secret" + } + volume_mount { + name = "data" + mount_path = "/root/.gramps/grampsdb" + sub_path = "grampsdb" + } + volume_mount { + name = "data" + mount_path = "/app/media" + sub_path = "media" + } + volume_mount { + name = "data" + mount_path = "/tmp" + sub_path = "tmp" + } + } + + container { + name = "grampsweb-celery" + image = "ghcr.io/gramps-project/grampsweb:latest" + command = ["celery", "-A", "gramps_webapi.celery", "worker", "--loglevel=INFO", "--concurrency=2"] + + dynamic "env" { + for_each = local.common_env + content { + name = env.value.name + value = env.value.value + } + } + + volume_mount { + name = "data" + mount_path = "/app/users" + sub_path = "users" + } + volume_mount { + name = "data" + mount_path = "/app/indexdir" + sub_path = "indexdir" + } + volume_mount { + name = "data" + mount_path = "/app/thumbnail_cache" + sub_path = "thumbnail_cache" + } + volume_mount { + name = "data" + mount_path = "/app/cache" + sub_path = "cache" + } + volume_mount { + name = "data" + mount_path = "/app/secret" + sub_path = "secret" + } + volume_mount { + name = "data" + mount_path = "/root/.gramps/grampsdb" + sub_path = "grampsdb" + } + volume_mount { + name = "data" + mount_path = "/app/media" + sub_path = "media" + } + volume_mount { + name = "data" + mount_path = "/tmp" + sub_path = "tmp" + } + } + + volume { + name = "data" + nfs { + server = "10.0.10.15" + path = "/mnt/main/grampsweb" + } + } + } + } + } +} + +resource "kubernetes_service" "grampsweb" { + metadata { + name = "grampsweb" + namespace = kubernetes_namespace.grampsweb.metadata[0].name + labels = { + app = "grampsweb" + } + } + + spec { + selector = { + app = "grampsweb" + } + port { + name = "http" + port = 80 + target_port = 5000 + } + } +} + +module "ingress" { + source = "../ingress_factory" + namespace = kubernetes_namespace.grampsweb.metadata[0].name + name = "family" + tls_secret_name = var.tls_secret_name + max_body_size = "500m" +} diff --git a/modules/kubernetes/main.tf b/modules/kubernetes/main.tf index 5bc91a97..077f0de4 100644 --- a/modules/kubernetes/main.tf +++ b/modules/kubernetes/main.tf @@ -149,7 +149,7 @@ locals { "url", "excalidraw", "travel_blog", "dashy", "send", "ytdlp", "wealthfolio", "rybbit", "stirling-pdf", "networking-toolbox", "navidrome", "freshrss", "forgejo", "tor-proxy", "real-estate-crawler", "n8n", "changedetection", "linkwarden", "matrix", "homepage", "meshcentral", "diun", "cyberchef", "ntfy", "ollama", - "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha", "affine", "plotting-book", "whisper" + "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha", "affine", "plotting-book", "whisper", "grampsweb" ], } active_modules = distinct(flatten([ @@ -1110,3 +1110,13 @@ module "whisper" { depends_on = [null_resource.core_services] } + +module "grampsweb" { + source = "./grampsweb" + for_each = contains(local.active_modules, "grampsweb") ? { grampsweb = true } : {} + tls_secret_name = var.tls_secret_name + smtp_password = var.mailserver_accounts["info@viktorbarzin.me"] + tier = local.tiers.aux + + depends_on = [null_resource.core_services] +}