updated app catalog with storage path option

This commit is contained in:
2026-02-12 07:35:56 +01:00
parent 82a8c8b6cf
commit 872949c3d7
9 changed files with 235 additions and 43 deletions
+28 -14
View File
@@ -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.* |
+31 -7
View File
@@ -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"
+10 -4
View File
@@ -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"
+29 -1
View File
@@ -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")
+5 -7
View File
@@ -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:
+6 -6
View File
@@ -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:
+3
View File
@@ -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!
+114
View File
@@ -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:
+9 -4
View File
@@ -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: