From 872949c3d7d408305c5377784123e65b111bed5b Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 12 Feb 2026 07:35:56 +0100 Subject: [PATCH] updated app catalog with storage path option --- README.md | 42 +++++--- customers/demo-felhom.yaml | 38 +++++-- customers/pi-customer-1.yaml | 14 ++- scripts/render.sh | 30 +++++- templates/filebrowser/docker-compose.yml | 12 +-- templates/immich/docker-compose.yml | 12 +-- templates/mealie/docker-compose.yml | 3 + templates/paperless-ngx/docker-compose.yml | 114 +++++++++++++++++++++ templates/romm/docker-compose.yml | 13 ++- 9 files changed, 235 insertions(+), 43 deletions(-) create mode 100644 templates/paperless-ngx/docker-compose.yml diff --git a/README.md b/README.md index 1b37107..fab18ea 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,24 @@ felhom-app-catalog/ ## How It Works -1. **Templates** contain Docker Compose files with `{{DOMAIN}}` placeholders -2. **Customer configs** define which apps each customer gets + any version overrides -3. **render.sh** generates per-customer Gitea repos with domain-substituted compose files +1. **Templates** contain Docker Compose files with `{{DOMAIN}}` and `{{HDD_PATH}}` placeholders +2. **Customer configs** define which apps each customer gets, their domain, HDD path, and any version overrides +3. **render.sh** generates per-customer Gitea repos with all placeholders substituted 4. **Portainer GitOps** on each customer node pulls from their repo and deploys +### Placeholder Reference + +| Placeholder | Source | Example | +|-------------|--------|---------| +| `{{DOMAIN}}` | `domain:` in customer YAML | `demo-felhom.eu` | +| `{{HDD_PATH}}` | `hdd_path:` in customer YAML | `/mnt/hdd_1` | + +### Storage Strategy + +- **HDD host paths** (`{{HDD_PATH}}/storage/...`): Large user data — photos, documents, ROMs +- **Named Docker volumes** (on NVMe): Databases, app config, caches — need fast I/O +- Templates that don't use `{{HDD_PATH}}` work without it (e.g. ActualBudget, Mealie) + ## Workflow ### Adding a new app to the catalog @@ -77,14 +90,15 @@ overrides: ## App Catalog -| App | DB Type | RAM Usage | Pi-Compatible | Description | -|-----|---------|-----------|---------------|-------------| -| ActualBudget | None (file) | ~50MB | ✅ | Personal finance / budgeting | -| Docmost | PostgreSQL + Redis | ~200MB | ⚠️ (heavy) | Wiki / documentation (Notion-like) | -| FileBrowser | None (file) | ~30MB | ✅ | Web file manager | -| Homebox | None (SQLite) | ~50MB | ✅ | Home inventory management | -| Immich | PostgreSQL + Redis | ~4GB | ❌ | Photo & video management | -| Mealie | None (SQLite) | ~200MB | ✅ | Recipe manager & meal planner | -| ROMM | MariaDB + Redis | ~300MB | ⚠️ | ROM / game library manager | -| Stirling-PDF | None | ~200MB | ✅ | PDF manipulation toolkit | -| Vaultwarden | None (SQLite) | ~50MB | ✅ | Password manager (Bitwarden) | +| App | DB Type | RAM | Pi | HDD Data | Subdomain | +|-----|---------|-----|-----|----------|-----------| +| ActualBudget | None (file) | ~50MB | ✅ | — | budget.* | +| Docmost | PostgreSQL + Redis | ~200MB | ⚠️ | — | docs.* | +| FileBrowser | None (file) | ~30MB | ✅ | `{{HDD_PATH}}/` | files.* | +| Homebox | None (SQLite) | ~50MB | ✅ | — | inventory.* | +| Immich | PostgreSQL + Redis | ~4GB | ❌ | `{{HDD_PATH}}/storage/immich/` | photos.* | +| Mealie | None (SQLite) | ~200MB | ✅ | — | recipes.* | +| Paperless-ngx | PostgreSQL + Redis | ~500MB | ✅ | `{{HDD_PATH}}/storage/paperless/` | paperless.* | +| ROMM | MariaDB + Redis | ~300MB | ⚠️ | `{{HDD_PATH}}/storage/romm/` | arcade.* | +| Stirling-PDF | None | ~200MB | ✅ | — | pdf.* | +| Vaultwarden | None (SQLite) | ~50MB | ✅ | — | vault.* | diff --git a/customers/demo-felhom.yaml b/customers/demo-felhom.yaml index 27305a1..daa0a99 100644 --- a/customers/demo-felhom.yaml +++ b/customers/demo-felhom.yaml @@ -4,6 +4,7 @@ customer_id: demo-felhom domain: demo-felhom.eu +hdd_path: /mnt/hdd_1 gitea_repo: customers/demo-felhom-stacks hardware: n100 notes: "Internal demo/test server for validating deployments" @@ -16,6 +17,7 @@ apps: - homebox - immich - mealie + - paperless-ngx - romm - stirling-pdf - vaultwarden @@ -34,6 +36,11 @@ env_vars_reference: DB_PASSWORD: "generate secure password" immich: DB_PASSWORD: "generate secure password" + paperless-ngx: + PAPERLESS_SECRET_KEY: "generate with: openssl rand -hex 32" + DB_PASSWORD: "generate secure password" + PAPERLESS_ADMIN_USER: "admin" + PAPERLESS_ADMIN_PASSWORD: "set initial password" romm: DB_PASSWORD: "generate secure password" MYSQL_ROOT_PASSWORD: "generate secure password" @@ -42,19 +49,36 @@ env_vars_reference: ADMIN_TOKEN: "generate with: openssl rand -hex 32" SIGNUPS_ALLOWED: "true (set to false after account creation)" +# Storage layout reference +# This shows where user data lives after render (HDD host paths): +# +# /mnt/hdd_1/ ← HDD root (filebrowser serves this) +# /mnt/hdd_1/storage/immich/ ← photos & videos +# /mnt/hdd_1/storage/paperless/consume/ ← drop documents here for OCR +# /mnt/hdd_1/storage/paperless/media/ ← processed documents +# /mnt/hdd_1/storage/paperless/export/ ← document exports / backup +# /mnt/hdd_1/storage/romm/library/ ← ROM files +# /mnt/hdd_1/storage/romm/resources/ ← cover art, metadata +# +# Named volumes (on NVMe, managed by Docker): +# actualbudget_data, docmost_*, homebox_data, mealie_data, +# immich_postgres_data, paperless_data, vaultwarden_data, etc. + # Backup considerations backup_notes: databases: - - "docmost: PostgreSQL (docmost-postgres container)" - - "immich: PostgreSQL (immich-postgres container)" - - "romm: MariaDB (romm-db container)" - file_volumes: + - "docmost: PostgreSQL (docmost-postgres)" + - "immich: PostgreSQL (immich-postgres)" + - "paperless-ngx: PostgreSQL (paperless-postgres)" + - "romm: MariaDB (romm-db)" + hdd_paths: + - "/mnt/hdd_1/storage/immich (photos — large, Backrest read-only mount)" + - "/mnt/hdd_1/storage/paperless/media (documents — Backrest read-only mount)" + - "/mnt/hdd_1/storage/romm/library (ROMs — Backrest read-only mount)" + named_volumes: - "actualbudget_data" - "docmost_storage" - - "filebrowser_data" - "homebox_data" - - "immich_upload (on HDD: /mnt/hdd_1/storage/immich)" - "mealie_data" - - "romm_library" - "stirling_data" - "vaultwarden_data" diff --git a/customers/pi-customer-1.yaml b/customers/pi-customer-1.yaml index 9185bc1..f32812b 100644 --- a/customers/pi-customer-1.yaml +++ b/customers/pi-customer-1.yaml @@ -4,6 +4,7 @@ customer_id: pi-customer-1 domain: pi-customer-1.local +hdd_path: /mnt/hdd_1 gitea_repo: customers/pi-customer-1-stacks hardware: rpi notes: "Test customer on Raspberry Pi — lightweight apps only" @@ -18,8 +19,6 @@ apps: # Per-customer overrides overrides: {} - # mealie_version: "v3.10.2" # Pin if needed - # auto_update: false # Portainer env vars to set (reference only) env_vars_reference: @@ -27,12 +26,19 @@ env_vars_reference: ADMIN_TOKEN: "generate with: openssl rand -hex 32" SIGNUPS_ALLOWED: "true (set to false after account creation)" +# Storage layout reference: +# /mnt/hdd_1/ ← HDD root (filebrowser serves this) +# +# Named volumes (on SD/USB boot, managed by Docker): +# actualbudget_data, mealie_data, stirling_data, vaultwarden_data + # Backup considerations backup_notes: databases: [] # No database containers — all apps use SQLite/file storage - file_volumes: + hdd_paths: + - "/mnt/hdd_1 (filebrowser root — user files)" + named_volumes: - "actualbudget_data" - - "filebrowser_data" - "mealie_data" - "stirling_data" - "vaultwarden_data" diff --git a/scripts/render.sh b/scripts/render.sh index 20789cf..a818de6 100644 --- a/scripts/render.sh +++ b/scripts/render.sh @@ -114,9 +114,10 @@ render_customer() { local customer_file="$1" local output_base="$2" - local customer_id domain + local customer_id domain hdd_path customer_id=$(yaml_get_value "$customer_file" "customer_id") domain=$(yaml_get_value "$customer_file" "domain") + hdd_path=$(yaml_get_value "$customer_file" "hdd_path") if [[ -z "$customer_id" || -z "$domain" ]]; then log_error "Missing customer_id or domain in: $customer_file" @@ -146,6 +147,9 @@ render_customer() { log_info "Apps: ${apps[*]}" log_info "Domain: $domain" + if [[ -n "$hdd_path" ]]; then + log_info "HDD path: $hdd_path" + fi # Output directory for this customer local customer_output="${output_base}/${customer_id}-stacks" @@ -154,6 +158,15 @@ render_customer() { echo -e " ${CYAN}[DRY-RUN]${NC} Would create: ${customer_output}/" for app in "${apps[@]}"; do echo -e " ${CYAN}[DRY-RUN]${NC} ${app}/docker-compose.yml ({{DOMAIN}} → ${domain})" + # Check if template uses HDD_PATH + local template="${TEMPLATES_DIR}/${app}/docker-compose.yml" + if [[ -f "$template" ]] && grep -q '{{HDD_PATH}}' "$template"; then + if [[ -n "$hdd_path" ]]; then + echo -e " ${CYAN}[DRY-RUN]${NC} ↳ HDD path: {{HDD_PATH}} → ${hdd_path}" + else + echo -e " ${YELLOW}[DRY-RUN]${NC} ↳ ⚠ Template uses {{HDD_PATH}} but hdd_path not set!" + fi + fi # Show version override if any local version_override version_override=$(yaml_get_override "$customer_file" "${app}_version") @@ -172,6 +185,7 @@ render_customer() { # ${customer_id} - Application Stacks **Domain:** \`${domain}\` +**HDD Path:** \`${hdd_path:-N/A (no HDD apps)}\` **Generated:** $(date -u '+%Y-%m-%d %H:%M:%S UTC') **Source:** felhom-app-catalog (render.sh) @@ -209,6 +223,20 @@ EOF # Substitute {{DOMAIN}} with customer domain sed "s/{{DOMAIN}}/${domain}/g" "$template" > "${customer_output}/${app}/docker-compose.yml" + # Substitute {{HDD_PATH}} if the template uses it + if grep -q '{{HDD_PATH}}' "${customer_output}/${app}/docker-compose.yml"; then + if [[ -z "$hdd_path" ]]; then + log_error " ${app}: template uses {{HDD_PATH}} but hdd_path not set in customer config!" + log_error " Add 'hdd_path: /mnt/hdd_1' (or similar) to ${customer_file}" + rm "${customer_output}/${app}/docker-compose.yml" + rmdir "${customer_output}/${app}" 2>/dev/null || true + continue + fi + # Remove trailing slash from hdd_path if present + local clean_hdd_path="${hdd_path%/}" + sed -i "s|{{HDD_PATH}}|${clean_hdd_path}|g" "${customer_output}/${app}/docker-compose.yml" + fi + # Apply version override if configured local version_override version_override=$(yaml_get_override "$customer_file" "${app}_version") diff --git a/templates/filebrowser/docker-compose.yml b/templates/filebrowser/docker-compose.yml index 136dab6..bce981f 100644 --- a/templates/filebrowser/docker-compose.yml +++ b/templates/filebrowser/docker-compose.yml @@ -6,13 +6,12 @@ # Environment variables (set in Portainer): # (none required) # +# Storage layout: +# Browsable files → {{HDD_PATH}} (HDD, host path — entire disk) +# App config/DB → filebrowser_config (named volume, NVMe) +# # First-time setup: # Default login: admin / admin — change immediately! -# -# Volume notes: -# filebrowser_data is the browsable file area. -# For HDD access, override this volume in Portainer: -# /mnt/hdd_1:/srv (or a specific subdirectory) services: filebrowser: @@ -24,7 +23,7 @@ services: - PUID=1000 - PGID=1000 volumes: - - filebrowser_data:/srv + - {{HDD_PATH}}:/srv - filebrowser_config:/database networks: - traefik-public @@ -42,7 +41,6 @@ services: - "traefik.http.services.filebrowser.loadbalancer.server.port=80" volumes: - filebrowser_data: filebrowser_config: networks: diff --git a/templates/immich/docker-compose.yml b/templates/immich/docker-compose.yml index 38cc564..0f27e00 100644 --- a/templates/immich/docker-compose.yml +++ b/templates/immich/docker-compose.yml @@ -6,10 +6,11 @@ # Environment variables (set in Portainer): # DB_PASSWORD - PostgreSQL password (required) # -# Volume notes: -# immich_upload is the photo/video storage location. -# For HDD storage, override in Portainer stack: -# /mnt/hdd_1/storage/immich:/usr/src/app/upload +# Storage layout: +# User photos/videos → {{HDD_PATH}}/storage/immich (HDD, host path) +# PostgreSQL data → immich_postgres_data (named volume, NVMe) +# ML model cache → immich_ml_cache (named volume, NVMe) +# Redis data → immich_redis_data (named volume, NVMe) # # First-time setup: # Create admin account on first visit. @@ -33,7 +34,7 @@ services: - IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003 - TZ=Europe/Budapest volumes: - - immich_upload:/usr/src/app/upload + - {{HDD_PATH}}/storage/immich:/usr/src/app/upload networks: - traefik-public - immich-internal @@ -104,7 +105,6 @@ services: retries: 3 volumes: - immich_upload: immich_ml_cache: immich_postgres_data: immich_redis_data: diff --git a/templates/mealie/docker-compose.yml b/templates/mealie/docker-compose.yml index a8594d4..47a764c 100644 --- a/templates/mealie/docker-compose.yml +++ b/templates/mealie/docker-compose.yml @@ -7,6 +7,9 @@ # (none required for basic usage) # SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD - for email features (optional) # +# Storage layout: +# Recipe data/images → mealie_data (named volume, NVMe — moderate size) +# # First-time setup: # Default login: changeme@example.com / MyPassword # Change immediately after first login! diff --git a/templates/paperless-ngx/docker-compose.yml b/templates/paperless-ngx/docker-compose.yml new file mode 100644 index 0000000..ebc2d2b --- /dev/null +++ b/templates/paperless-ngx/docker-compose.yml @@ -0,0 +1,114 @@ +# Paperless-ngx - Document Management System (DMS) +# Domain: docs.{{DOMAIN}} +# Database: PostgreSQL + Redis +# RAM: ~500MB (more with OCR/Tika) | Pi-compatible: Yes (arm64, 4GB+ RAM recommended) +# +# Environment variables (set in Portainer): +# PAPERLESS_SECRET_KEY - Random secret (required, generate with: openssl rand -hex 32) +# DB_PASSWORD - PostgreSQL password (required) +# PAPERLESS_ADMIN_USER - Initial admin username (optional, default: admin) +# PAPERLESS_ADMIN_PASSWORD - Initial admin password (optional) +# +# Storage layout: +# Consume folder → {{HDD_PATH}}/storage/paperless/consume (HDD, drop files here) +# Document media → {{HDD_PATH}}/storage/paperless/media (HDD, originals + archive) +# Export folder → {{HDD_PATH}}/storage/paperless/export (HDD, for backups) +# App data/index → paperless_data (named volume, NVMe) +# PostgreSQL data → paperless_postgres_data (named volume, NVMe) +# Redis data → paperless_redis_data (named volume, NVMe) +# +# First-time setup: +# If PAPERLESS_ADMIN_USER/PASSWORD env vars are set, admin is auto-created. +# Otherwise: docker exec -it paperless-webserver createsuperuser + +services: + paperless-webserver: + image: ghcr.io/paperless-ngx/paperless-ngx:2.15.3 + container_name: paperless-webserver + restart: unless-stopped + depends_on: + paperless-postgres: + condition: service_healthy + paperless-redis: + condition: service_healthy + environment: + - PAPERLESS_REDIS=redis://paperless-redis:6379 + - PAPERLESS_DBHOST=paperless-postgres + - PAPERLESS_DBUSER=paperless + - PAPERLESS_DBPASS=${DB_PASSWORD} + - PAPERLESS_DBNAME=paperless + - PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY} + - PAPERLESS_URL=https://paperless.{{DOMAIN}} + - PAPERLESS_TIME_ZONE=Europe/Budapest + - PAPERLESS_OCR_LANGUAGE=hun+eng + - PAPERLESS_ADMIN_USER=${PAPERLESS_ADMIN_USER:-} + - PAPERLESS_ADMIN_PASSWORD=${PAPERLESS_ADMIN_PASSWORD:-} + - PAPERLESS_CONSUMER_POLLING=30 + - PAPERLESS_TASK_WORKERS=2 + - PAPERLESS_THREADS_PER_WORKER=1 + - USERMAP_UID=1000 + - USERMAP_GID=1000 + volumes: + - paperless_data:/usr/src/paperless/data + - {{HDD_PATH}}/storage/paperless/media:/usr/src/paperless/media + - {{HDD_PATH}}/storage/paperless/consume:/usr/src/paperless/consume + - {{HDD_PATH}}/storage/paperless/export:/usr/src/paperless/export + networks: + - traefik-public + - paperless-internal + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + labels: + - "traefik.enable=true" + - "traefik.http.routers.paperless.rule=Host(`paperless.{{DOMAIN}}`)" + - "traefik.http.routers.paperless.entrypoints=websecure" + - "traefik.http.routers.paperless.tls=true" + - "traefik.http.services.paperless.loadbalancer.server.port=8000" + + paperless-postgres: + image: postgres:16-alpine + container_name: paperless-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=paperless + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=paperless + volumes: + - paperless_postgres_data:/var/lib/postgresql/data + networks: + - paperless-internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U paperless -d paperless"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + + paperless-redis: + image: redis:7-alpine + container_name: paperless-redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - paperless_redis_data:/data + networks: + - paperless-internal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + +volumes: + paperless_data: + paperless_postgres_data: + paperless_redis_data: + +networks: + traefik-public: + external: true + paperless-internal: diff --git a/templates/romm/docker-compose.yml b/templates/romm/docker-compose.yml index fdfdf44..6e5fa01 100644 --- a/templates/romm/docker-compose.yml +++ b/templates/romm/docker-compose.yml @@ -11,6 +11,13 @@ # IGDB_CLIENT_SECRET - IGDB API client secret (optional) # STEAMGRIDDB_API_KEY - SteamGridDB API key (optional, for cover art) # +# Storage layout: +# ROM library → {{HDD_PATH}}/storage/romm/library (HDD, host path) +# Cover art etc → {{HDD_PATH}}/storage/romm/resources (HDD, host path) +# App config → romm_config (named volume, NVMe) +# MariaDB data → romm_db_data (named volume, NVMe) +# Redis data → romm_redis_data (named volume, NVMe) +# # First-time setup: # Default login: admin / admin — change immediately! @@ -53,8 +60,8 @@ services: - STEAMGRIDDB_API_KEY=${STEAMGRIDDB_API_KEY:-} - TZ=Europe/Budapest volumes: - - romm_library:/romm/library - - romm_resources:/romm/resources + - {{HDD_PATH}}/storage/romm/library:/romm/library + - {{HDD_PATH}}/storage/romm/resources:/romm/resources - romm_config:/romm/config networks: - traefik-public @@ -103,8 +110,6 @@ services: retries: 3 volumes: - romm_library: - romm_resources: romm_config: romm_db_data: romm_redis_data: