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
- Templates in this repo contain Docker Compose files with
${DOMAIN},${HDD_PATH}, and${SECRET}placeholders generate-customer.shsubstitutes all values (domain, HDD path, auto-generated passwords) and produces customer-specific compose files + a Portainertemplates.jsonwith zero env varsdocker-setup.shon the customer node starts Portainer with--templatespointing at the customer's generatedtemplates.json- Customer opens Portainer -> Templates -> picks an app -> clicks Deploy -> done
Workflow
1. Generate customer templates (on your workstation)
# 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
sudo ./docker-setup.sh --domain demo-felhom.eu --customer demo-felhom \
--email certs@felhom.eu --cf-token <cloudflare-api-token>
3. Deploy apps
Open https://portainer.<domain> -> 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.
# 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:
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
- Create
templates/<appname>/docker-compose.ymlusing${DOMAIN}and optionally${HDD_PATH} - Add a template entry in
templates.jsonwith env definitions, description, logo, and notes - If the app needs secrets, add entries to
APP_SECRET_DEFSingenerate-customer.sh - Re-run
generate-customer.sh --pushfor each customer
templates.json entry format
{
"type": 3,
"title": "App Name",
"description": "Short description.",
"categories": ["category"],
"platform": "linux",
"logo": "https://example.com/logo.png",
"note": "<b>Access:</b> 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 | This repo -- templates + generation script |
| customers-felhom.eu | Generated per-customer compose files + templates |
| deploy-portainer | docker-setup.sh -- server provisioning |