# Felhom App Catalog Central repository for Felhom customer application templates. ## Architecture ``` app-catalog-felhom.eu/ <- This repo (source of truth) ├── templates.json # Portainer App Templates index (generic, with env var prompts) ├── templates/ # Docker Compose templates with ${VAR} env var syntax │ ├── actualbudget/ │ ├── docmost/ │ ├── filebrowser/ │ ├── homebox/ │ ├── immich/ │ ├── mealie/ │ ├── paperless-ngx/ │ ├── romm/ │ ├── stirling-pdf/ │ └── vaultwarden/ └── scripts/ └── generate-customer.sh # Generates customer-specific templates with baked-in secrets ``` The output is pushed to: **https://gitea.dooplex.hu/admin/customers-felhom.eu** ``` customers-felhom.eu/ <- Generated per-customer deployments ├── demo-felhom/ │ ├── templates.json # Customer Portainer templates (zero env vars, zero-touch deploy) │ ├── secrets.env # Reference copy of all generated secrets │ ├── actualbudget/docker-compose.yml │ ├── docmost/docker-compose.yml │ └── ... └── pi-customer-1/ ├── templates.json ├── secrets.env ├── actualbudget/docker-compose.yml └── ... ``` ## How It Works 1. **Templates** in this repo contain Docker Compose files with `${DOMAIN}`, `${HDD_PATH}`, and `${SECRET}` placeholders 2. **`generate-customer.sh`** substitutes all values (domain, HDD path, auto-generated passwords) and produces customer-specific compose files + a Portainer `templates.json` with zero env vars 3. **`docker-setup.sh`** on the customer node starts Portainer with `--templates` pointing at the customer's generated `templates.json` 4. **Customer** opens Portainer -> Templates -> picks an app -> clicks Deploy -> **done** ## Workflow ### 1. Generate customer templates (on your workstation) ```bash # Full setup: all apps, with HDD ./scripts/generate-customer.sh --customer demo-felhom \ --domain demo-felhom.eu --hdd-path /mnt/hdd_1 --push # Raspberry Pi: lightweight apps only, no HDD yet ./scripts/generate-customer.sh --customer pi-customer-1 \ --domain pi-customer-1.local \ --apps actualbudget,filebrowser,mealie,stirling-pdf,vaultwarden --push # Preview without creating files ./scripts/generate-customer.sh --customer test --domain test.local --dry-run ``` Options: - `--customer ID` -- Customer identifier (required) - `--domain DOMAIN` -- Customer domain (required) - `--hdd-path PATH` -- External HDD mount path (optional; apps needing it show a field in Portainer if omitted) - `--apps LIST` -- Comma-separated app list (default: all available) - `--push` -- Git commit and push to customers-felhom.eu repo - `--dry-run` -- Show what would be done - `--debug` -- Verbose output ### 2. Set up customer server ```bash sudo ./docker-setup.sh --domain demo-felhom.eu --customer demo-felhom \ --email certs@felhom.eu --cf-token ``` ### 3. Deploy apps Open `https://portainer.` -> **Templates** -> pick an app -> **Deploy the stack**. All fields (domain, passwords, secrets) are pre-filled. If HDD_PATH wasn't set during generation, apps that need it will show one field to fill in. ### Re-generating after changes Re-running `generate-customer.sh` for an existing customer preserves all previously generated secrets (idempotent). Only new apps or new secrets are generated. ```bash # Add an app to an existing customer ./scripts/generate-customer.sh --customer demo-felhom \ --domain demo-felhom.eu --hdd-path /mnt/hdd_1 --push ``` ## Environment Variables & Secrets Secrets are auto-generated by `generate-customer.sh` and baked directly into the compose files and Portainer templates. They are **not** stored separately on the customer node -- they live in the customers-felhom.eu Git repo (which is private on your Gitea). A `secrets.env` reference file is generated alongside the compose files for easy lookup. ### Secret definitions Defined in `generate-customer.sh` via the `APP_SECRET_DEFS` array: ```bash APP_SECRET_DEFS=( "docmost:APP_SECRET:hex:32" "docmost:DB_PASSWORD:password:24" "immich:DB_PASSWORD:password:24" "paperless-ngx:PAPERLESS_SECRET_KEY:hex:32" "paperless-ngx:DB_PASSWORD:password:24" "paperless-ngx:PAPERLESS_ADMIN_USER:static:admin" "paperless-ngx:PAPERLESS_ADMIN_PASSWORD:password:16" "romm:DB_PASSWORD:password:24" "romm:MYSQL_ROOT_PASSWORD:password:24" "romm:ROMM_AUTH_SECRET_KEY:hex:32" "vaultwarden:ADMIN_TOKEN:hex:32" "vaultwarden:SIGNUPS_ALLOWED:static:true" ) ``` Types: `password:LENGTH` (alphanumeric), `hex:LENGTH` (crypto), `static:VALUE` (fixed). ### Variable types per app | App | DOMAIN | HDD_PATH | Secrets | |-----|:------:|:--------:|---------| | ActualBudget | yes | -- | -- | | Docmost | yes | -- | APP_SECRET, DB_PASSWORD | | FileBrowser | yes | yes | -- | | Homebox | yes | -- | -- | | Immich | yes | yes | DB_PASSWORD | | Mealie | yes | -- | -- | | Paperless-ngx | yes | yes | PAPERLESS_SECRET_KEY, DB_PASSWORD, PAPERLESS_ADMIN_USER, PAPERLESS_ADMIN_PASSWORD | | ROMM | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, ROMM_AUTH_SECRET_KEY | | Stirling-PDF | yes | -- | -- | | Vaultwarden | yes | -- | ADMIN_TOKEN | ## App Catalog | App | DB Type | RAM | Pi | HDD Data | Subdomain | |-----|---------|-----|-----|----------|-----------| | ActualBudget | None (file) | ~50MB | yes | -- | budget.* | | Docmost | PostgreSQL + Redis | ~200MB | maybe | -- | docs.* | | FileBrowser | None (file) | ~30MB | yes | `${HDD_PATH}/storage/filebrowser/` | files.* | | Homebox | None (SQLite) | ~50MB | yes | -- | inventory.* | | Immich | PostgreSQL + Redis | ~4GB | no | `${HDD_PATH}/storage/immich/` | photos.* | | Mealie | None (SQLite) | ~200MB | yes | -- | recipes.* | | Paperless-ngx | PostgreSQL + Redis | ~500MB | yes | `${HDD_PATH}/storage/paperless/` | paperless.* | | ROMM | MariaDB + Redis | ~300MB | maybe | `${HDD_PATH}/storage/romm/` | arcade.* | | Stirling-PDF | None | ~200MB | yes | -- | pdf.* | | Vaultwarden | None (SQLite) | ~50MB | yes | -- | vault.* | ### Storage strategy - **HDD host paths** (`${HDD_PATH}/storage/...`): Large user data -- photos, documents, ROMs - **Named Docker volumes** (on internal SSD): Databases, app config, caches -- need fast I/O - Templates without `${HDD_PATH}` work without an external HDD (e.g., ActualBudget, Mealie) ## Adding a New App 1. Create `templates//docker-compose.yml` using `${DOMAIN}` and optionally `${HDD_PATH}` 2. Add a template entry in `templates.json` with env definitions, description, logo, and notes 3. If the app needs secrets, add entries to `APP_SECRET_DEFS` in `generate-customer.sh` 4. Re-run `generate-customer.sh --push` for each customer ### templates.json entry format ```json { "type": 3, "title": "App Name", "description": "Short description.", "categories": ["category"], "platform": "linux", "logo": "https://example.com/logo.png", "note": "Access: https://subdomain.<DOMAIN>", "repository": { "url": "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu", "stackfile": "templates/appname/docker-compose.yml" }, "env": [ { "name": "DOMAIN", "label": "Domain", "description": "Your server domain (e.g., demo-felhom.eu)" } ] } ``` ## Related Repositories | Repository | Purpose | |------------|---------| | [app-catalog-felhom.eu](https://gitea.dooplex.hu/admin/app-catalog-felhom.eu) | This repo -- templates + generation script | | [customers-felhom.eu](https://gitea.dooplex.hu/admin/customers-felhom.eu) | Generated per-customer compose files + templates | | [deploy-portainer](https://gitea.dooplex.hu/admin/deploy-portainer) | `docker-setup.sh` -- server provisioning |