dc0ab8b2a8
gates / gates (push) Successful in 2s
NEW-APP-CHECKLIST.md: the reviewer's draft reviewed - 60 rows in 10 groups, each with how/why and a since date; 7 rows added, 16 sharpened, 9 wrong claims fixed. onboarding/_TEMPLATE.md (one line per id), onboarding/wger.md (the pilot, exempt app, 11 open rows each a register row), onboarding/EXISTING-APPS-GAPS.md (read only, from scripts/onboarding_gaps.py). Gate onboarding (scripts/check-onboarding.py) in --fast: a template directory not among the 53 published before 2026-10-01 needs a complete record; decoys in test_gate_decoys.py (16 cases, 5 gate mutants seen red). CLAUDE.md, REUSE.md 5, README point to it. No template changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
445 lines
20 KiB
Markdown
445 lines
20 KiB
Markdown
# 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 (LEGACY — Portainer-only setups)
|
|
├── templates/ # Docker Compose templates with ${VAR} env var syntax
|
|
│ ├── actualbudget/
|
|
│ │ ├── docker-compose.yml
|
|
│ │ └── .felhom.yml # App metadata for felhom-controller
|
|
│ ├── adventurelog/
|
|
│ ├── audiobookshelf/
|
|
│ ├── bentopdf/
|
|
│ ├── bookstack/
|
|
│ ├── calcom/
|
|
│ ├── calibre-web/
|
|
│ ├── claper/
|
|
│ ├── code-server/
|
|
│ ├── crafty-controller/
|
|
│ ├── docmost/
|
|
│ ├── emby/
|
|
│ ├── filebrowser/
|
|
│ ├── ghost/
|
|
│ ├── gitea/
|
|
│ ├── glance/
|
|
│ ├── gokapi/
|
|
│ ├── grafana/
|
|
│ ├── gramps-web/
|
|
│ ├── home-assistant/
|
|
│ ├── homebox/
|
|
│ ├── homepage/
|
|
│ ├── immich/
|
|
│ ├── jellyfin/
|
|
│ ├── kimai/
|
|
│ ├── komga/
|
|
│ ├── mealie/
|
|
│ ├── n8n/
|
|
│ ├── navidrome/
|
|
│ ├── nextcloud/
|
|
│ ├── onlyoffice/
|
|
│ ├── opengist/
|
|
│ ├── outline/
|
|
│ ├── paperless-ngx/
|
|
│ ├── papra/
|
|
│ ├── plex/
|
|
│ ├── privatebin/
|
|
│ ├── radarr/
|
|
│ ├── rallly/
|
|
│ ├── romm/
|
|
│ ├── seerr/
|
|
│ ├── sonarr/
|
|
│ ├── sparkyfitness/
|
|
│ ├── tandoor/
|
|
│ ├── termix/
|
|
│ ├── uptime-kuma/
|
|
│ ├── vaultwarden/
|
|
│ ├── vikunja/
|
|
│ ├── wanderer/
|
|
│ ├── wger/
|
|
│ ├── wishlist/
|
|
│ └── zipline/
|
|
└── scripts/
|
|
└── generate-customer.sh # LEGACY — generates customer-specific templates for Portainer
|
|
```
|
|
|
|
## How It Works (Controller-based deployments)
|
|
|
|
The **felhom-controller** syncs directly from this repo:
|
|
|
|
1. Controller periodically pulls this repo (configurable interval, default 15m)
|
|
2. Copies `docker-compose.yml` and `.felhom.yml` from `templates/<app>/` to `/opt/docker/stacks/<app>/`
|
|
3. **Never overwrites** `app.yaml` (deployed config/secrets) or `.env` files
|
|
4. Only copies files if content has actually changed (SHA-256 comparison)
|
|
5. After sync, triggers a stack rescan so new/updated apps appear on the dashboard
|
|
6. Manual sync available via "Sablonok frissítése" button on the Alkalmazások page
|
|
|
|
### Controller git config (in controller.yaml)
|
|
|
|
```yaml
|
|
git:
|
|
repo_url: "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git"
|
|
branch: "main"
|
|
sync_interval: "15m"
|
|
username: "" # Optional, for private repos
|
|
token: "" # Optional, for private repos
|
|
```
|
|
|
|
## .felhom.yml Format
|
|
|
|
Each app template has a `.felhom.yml` metadata file that the controller uses for:
|
|
- Display info on the dashboard (name, description, category, subdomain)
|
|
- Resource hints (memory request/limit, Pi compatibility, HDD requirement)
|
|
- Deploy fields (what the user fills in during first deployment)
|
|
|
|
### Field types
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `domain` | Auto-filled from controller config, read-only |
|
|
| `secret` | Auto-generated, hidden from user (user sees "Generated ✓") |
|
|
| `password` | Auto-generated but shown, user can override |
|
|
| `path` | Filesystem path (validated for existence) |
|
|
| `text` | Free text input |
|
|
| `select` | Dropdown with predefined options |
|
|
| `boolean` | Toggle switch |
|
|
|
|
### Generator types (for secret/password fields)
|
|
|
|
| Generator | Description |
|
|
|-----------|-------------|
|
|
| `password:N` | N chars alphanumeric |
|
|
| `hex:N` | N bytes hex-encoded |
|
|
| `base64key:N` | `base64:` + N random bytes base64-encoded (Laravel APP_KEY format) |
|
|
| `static:VAL` | Fixed value |
|
|
|
|
### Example .felhom.yml
|
|
|
|
```yaml
|
|
display_name: "Paperless-ngx"
|
|
description: "Dokumentumok digitalizálása és rendszerezése"
|
|
category: "productivity"
|
|
subdomain: "paperless"
|
|
slug: "paperless-ngx"
|
|
resources:
|
|
mem_request: "500M"
|
|
mem_limit: "1152M"
|
|
pi_compatible: true
|
|
needs_hdd: true
|
|
deploy_fields:
|
|
- env_var: DOMAIN
|
|
label: "Domain"
|
|
type: domain
|
|
locked_after_deploy: true
|
|
- env_var: DB_PASSWORD
|
|
label: "Adatbázis jelszó"
|
|
type: secret
|
|
generate: "password:24"
|
|
locked_after_deploy: true
|
|
- env_var: HDD_PATH
|
|
label: "Adattárolási útvonal"
|
|
type: path
|
|
required: true
|
|
placeholder: "/mnt/felhom-drives/hdd_1"
|
|
locked_after_deploy: true
|
|
```
|
|
|
|
### Known default login (`after_install`, controller ≥ 0.279.0)
|
|
|
|
An app that starts with a known admin login replaces it right after a FRESH install (`09` §3 decision 45):
|
|
|
|
```yaml
|
|
deploy_fields:
|
|
- env_var: ADMIN_PASSWORD # shown on the app page as the first password
|
|
type: password
|
|
generate: "password:24"
|
|
locked_after_deploy: true
|
|
after_install:
|
|
service: bookstack # the app's own container
|
|
env: [ADMIN_PASSWORD] # only these deploy values are filled into ${NAME}
|
|
command: ["php", "/app/www/artisan", "bookstack:create-admin", "--email=admin@admin.com", "--name=Admin", "--password=${ADMIN_PASSWORD}", "--initial"]
|
|
success: "The default admin user has been updated" # the output must carry this
|
|
```
|
|
|
|
Never run after a restore or a kept-data load. Keep `app_info.default_creds`: the page hides it once the command
|
|
succeeded, and says "This app starts with a known, shared password…" while it is still in effect. Per-app status:
|
|
`FIRST-ADMIN.md`.
|
|
|
|
**Never paste `${ADMIN_PASSWORD}` into program code** (a `python3 -c` string, a Django `shell -c`, an Elixir `rpc`
|
|
string): a quote in a household-typed password breaks or changes the program. Pass it as its OWN argument
|
|
(`["python3", "-c", "...sys.argv[1]...", "${ADMIN_PASSWORD}"]`, mealie and wger) or inside a plain argument
|
|
(`--password=${ADMIN_PASSWORD}`, calibre-web's `admin:${ADMIN_PASSWORD}`). An app whose policy demands a special
|
|
character uses `generate: "password:24:special"` (controller ≥ 0.280.0: a lower, an upper, a digit and one of
|
|
`-_.!@#%+=`).
|
|
|
|
### The setup gate (`setup_gate`, controller ≥ 0.280.0)
|
|
|
|
An app whose FIRST VISITOR creates the admin is installed closed to everyone but the household (a browser signed in
|
|
to the dashboard) until its first setup is done (`09` §3 decision 46):
|
|
|
|
```yaml
|
|
setup_gate: true
|
|
setup_done_probe: # optional — without it the household presses "Done, I set it up"
|
|
url: http://n8n:5678/rest/settings # the app's own read-only status, on the docker network
|
|
field: data.userManagement.showSetupOnFirstLoad # dotted JSON path
|
|
done: "false" # its value once an admin exists, as text
|
|
```
|
|
|
|
The url uses the app's `container_name` and its internal port. **Measure the probe on 9202 before and after the
|
|
setup** (it must flip), and prove a stranger gets the gate page / 401 until then. Per-app status: `FIRST-ADMIN.md`.
|
|
|
|
A probe must answer **HTTP 200 with a JSON object** on both sides of the setup (the box reads only that; a 403/405
|
|
or a list never opens the gate, and it also blocks the household's press — gramps-web, ghost, home-assistant).
|
|
|
|
### Open sign-up after the first admin (`signup_block`, controller ≥ 0.281.0)
|
|
|
|
`09` §3 decision 47: once the gate opens, a stranger cannot make an account. Where the app itself keeps sign-up open,
|
|
the template names its sign-up address as a traefik matcher; the box answers it with "sign-up is closed" from then on,
|
|
and the household can open it for 15 minutes from the app page. `add_people:` (hu, + `i18n.en`) tells them how.
|
|
|
|
```yaml
|
|
signup_block: "PathPrefix(`/-/register`)" # opengist
|
|
app_info:
|
|
add_people: "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál."
|
|
```
|
|
|
|
**Measure it on 9202:** after the setup a stranger's sign-up succeeds (the reason for the block), with the block it is
|
|
refused, and the rest of the app still answers.
|
|
|
|
**Two locks since controller 0.282.0.** Where the app has its OWN sign-up switch, wire it to `SIGNUP_CLOSED` /
|
|
`SIGNUP_OPEN` with an OPEN default in the compose (`- DISABLE_REGISTRATION=${SIGNUP_CLOSED:-false}`) — so an app
|
|
installed before is unchanged by the catalog — and declare `after_setup: {env: {SIGNUP_CLOSED: "true"}}`. The box sets
|
|
it when the gate opens (or on "close sign-up now") and starts the app once. Measure whether the switch also blocks the
|
|
household's own first account (it does on 6 of 9); `after_setup` avoids that either way.
|
|
|
|
**Write the block case-insensitive and slash-tolerant:** `PathRegexp(`(?i)^/+api/+v1/+register`)`. Measured
|
|
2026-09-29: termix's router ignores case (`/users/CREATE` reached its sign-up), and PocketBase takes a collection name in
|
|
any case and by its id. Test every block with `felhom.eu/documentation/audits/signup-lock-2026-09-29/B/tricks.py`.
|
|
|
|
**`setup_done_probe:` must carry its measurement** in the comment directly above it — "measured", a date and both
|
|
answers (`false -> true`, or "before … after …"). Gate: `scripts/check-probe-measured.py`. A probe may index a list
|
|
(`setup.0.status`) and may count one non-200 status as done (`done_status: 405`, controller ≥ 0.282.0).
|
|
|
|
### App-email mapping (`smtp_mapping`)
|
|
|
|
Apps that can send outbound email (password resets, invites, confirmations) get it through
|
|
one managed path: **app → in-controller SMTP shim → hub → Resend**. The Resend key stays
|
|
hub-side; nothing app-specific lives on the box. An app opts in by declaring `smtp_mapping`,
|
|
which renames the generic relay settings to that app's own env-var names.
|
|
|
|
When app-email is on (the household's **global** toggle AND the app's **per-app** toggle),
|
|
the controller injects at deploy/redeploy: `host` = the on-box shim, `port` = `2525`,
|
|
`from` = `<from_local>@felhom.eu`, the `security` value, the optional display name, and any
|
|
fixed `extra` vars. The values are derived from settings on every compose — never written to
|
|
`app.yaml` — so a toggle change applies on the next redeploy without touching secrets. The
|
|
shim accepts **no-auth** on the Docker network, so leave any `SMTP_USERNAME`/`SMTP_PASSWORD`
|
|
unset.
|
|
|
|
**The compose file must reference the mapped `${VAR}` keys** (with a harmless default) so the
|
|
injected values reach the container, e.g. `- SMTP_HOST=${SMTP_HOST:-}`. An empty `SMTP_HOST`
|
|
keeps the app's mail disabled when the toggle is off.
|
|
|
|
| Field | Meaning |
|
|
|-------|---------|
|
|
| `host_var` | env key receiving the shim host (required) |
|
|
| `port_var` | env key receiving the port `2525` (required) |
|
|
| `security_var` | env key receiving the TLS mode (optional) |
|
|
| `security_value` | the app's term for the chosen mode — `starttls`, `TLS`, `NONE`, … |
|
|
| `from_var` | env key receiving the From address (required) |
|
|
| `from_name_var` | env key receiving the From display name (optional) |
|
|
| `from_local` | local-part of the From address (defaults to the app slug) → `<local>@felhom.eu` |
|
|
| `extra` | fixed extra env (e.g. accept-invalid-cert flags) |
|
|
|
|
```yaml
|
|
# Vaultwarden — STARTTLS to the shim (it accepts the self-signed cert):
|
|
smtp_mapping:
|
|
host_var: SMTP_HOST
|
|
port_var: SMTP_PORT
|
|
security_var: SMTP_SECURITY
|
|
security_value: starttls
|
|
from_var: SMTP_FROM
|
|
from_name_var: SMTP_FROM_NAME
|
|
from_local: vaultwarden
|
|
extra:
|
|
SMTP_ACCEPT_INVALID_CERTS: "true"
|
|
SMTP_ACCEPT_INVALID_HOSTNAMES: "true"
|
|
```
|
|
|
|
> **TLS choice per app.** Prefer STARTTLS to the shim for apps that can accept a self-signed
|
|
> cert (an "accept invalid certs" option). For apps that **can't** (e.g. Mealie), use
|
|
> plaintext (`security_value: "NONE"`) on `2525` — the shim offers it on the Docker network and
|
|
> it was spike-validated. Never publish the shim off-box, so plaintext there is safe.
|
|
|
|
Currently mapped: **Vaultwarden**, **Mealie** (the two apps proven in
|
|
`SPIKE-smtp-app-relay-2026-06-28`). Adding email to another app is just its `smtp_mapping`
|
|
block + the matching compose `${VAR}` lines.
|
|
|
|
## App Catalog
|
|
|
|
| App | DB Type | RAM (request / limit) | Pi | HDD Data | Subdomain |
|
|
|-----|---------|----------------------|-----|----------|-----------|
|
|
| ActualBudget | None (file) | 50M / 256M | yes | -- | budget.* |
|
|
| AdventureLog | PostgreSQL | 100M / 384M | yes | -- | travel.* |
|
|
| Audiobookshelf | None (file) | 100M / 512M | yes | `${HDD_PATH}/media/audiobooks/` | audiobooks.* |
|
|
| BentoPDF | None (file) | 100M / 384M | yes | -- | pdf.* |
|
|
| BookStack | MariaDB | 150M / 512M | yes | -- | wiki.* |
|
|
| Cal.com | PostgreSQL | 200M / 768M | no | -- | cal.* |
|
|
| Calibre-Web Automated | None (file) | 200M / 768M | no | `${HDD_PATH}/media/books/` | books.* |
|
|
| Claper | PostgreSQL | 100M / 384M | yes | -- | present.* |
|
|
| Code-Server | None (file) | 200M / 1024M | no | -- | code.* |
|
|
| Crafty Controller | None (file) | 256M / 2048M | no | -- | minecraft.* |
|
|
| Docmost | PostgreSQL + Redis | 200M / 768M | no | -- | docs.* |
|
|
| Emby | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | emby.* |
|
|
| FileBrowser Quantum | None (file) | 50M / 256M | yes | `${HDD_PATH}/storage/filebrowser/` | files.* |
|
|
| Ghost | SQLite | 150M / 512M | no | -- | blog.* |
|
|
| Gitea | SQLite | 100M / 512M | yes | -- | git.* |
|
|
| Glance | None (file) | 20M / 128M | yes | -- | dashboard.* |
|
|
| Gokapi | None (file) | 30M / 128M | yes | -- | share.* |
|
|
| Grafana | None (file) | 100M / 512M | yes | -- | grafana.* |
|
|
| Gramps Web | None (file) | 100M / 384M | yes | -- | family.* |
|
|
| Home Assistant | None (file) | 256M / 1024M | yes | -- | ha.* |
|
|
| Homebox | None (SQLite) | 50M / 256M | yes | -- | inventory.* |
|
|
| Homepage | None (file) | 50M / 256M | yes | -- | home.* |
|
|
| Immich | PostgreSQL + Redis | 2048M / 4096M | no | `${HDD_PATH}/storage/immich/` | photos.* |
|
|
| Jellyfin | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | media.* |
|
|
| Kimai | MariaDB | 100M / 384M | yes | -- | time.* |
|
|
| Komga | None (file) | 200M / 512M | yes | `${HDD_PATH}/media/comics/` | comics.* |
|
|
| Mealie | None (SQLite) | 200M / 1000M | yes | -- | recipes.* |
|
|
| n8n | None (file) | 150M / 512M | no | -- | auto.* |
|
|
| Navidrome | None (file) | 50M / 256M | yes | `${HDD_PATH}/media/music/` | music.* |
|
|
| Nextcloud | MariaDB + Redis | 256M / 1024M | no | `${HDD_PATH}/storage/nextcloud/` | cloud.* |
|
|
| OnlyOffice | None (file) | 512M / 2048M | no | -- | office.* |
|
|
| OpenGist | None (file) | 30M / 128M | yes | -- | gist.* |
|
|
| Outline | PostgreSQL + Redis | 200M / 768M | no | -- | kb.* |
|
|
| Paperless-ngx | PostgreSQL + Redis | 500M / 1152M | yes | `${HDD_PATH}/storage/paperless/` | paperless.* |
|
|
| Papra | None (file) | 50M / 256M | yes | -- | papra.* |
|
|
| Plant-it | None (file) | 50M / 256M | yes | -- | plants.* |
|
|
| Plex | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | plex.* |
|
|
| PrivateBin | None (file) | 30M / 128M | yes | -- | paste.* |
|
|
| Radarr | None (file) | 150M / 512M | yes | `${HDD_PATH}/media/` | radarr.* |
|
|
| Rallly | PostgreSQL | 50M / 256M | yes | -- | poll.* |
|
|
| RomM | MariaDB + Redis | 300M / 1024M | no | `${HDD_PATH}/storage/romm/` | arcade.* |
|
|
| Jellyseerr | None (file) | 100M / 384M | yes | -- | requests.* |
|
|
| Sonarr | None (file) | 150M / 512M | yes | `${HDD_PATH}/media/` | sonarr.* |
|
|
| SparkyFitness | PostgreSQL | 400M / 1792M | no | -- | sparky.* |
|
|
| Tandoor Recipes | PostgreSQL | 150M / 512M | yes | -- | recipes.* |
|
|
| Termix | None (file) | 30M / 128M | yes | -- | terminal.* |
|
|
| Uptime Kuma | None (file) | 50M / 256M | yes | -- | status.* |
|
|
| Vaultwarden | None (SQLite) | 50M / 256M | yes | -- | vault.* |
|
|
| Vikunja | None (file) | 50M / 256M | yes | -- | tasks.* |
|
|
| Wanderer | None (file) | 100M / 384M | yes | -- | hike.* |
|
|
| wger | SQLite | 100M / 384M | yes | -- | fitness.* |
|
|
| Wishlist | None (file) | 30M / 128M | yes | -- | wishes.* |
|
|
| Zipline | PostgreSQL | 100M / 512M | no | -- | img.* |
|
|
|
|
### Variable types per app
|
|
|
|
| App | DOMAIN | HDD_PATH | Secrets |
|
|
|-----|:------:|:--------:|---------|
|
|
| ActualBudget | yes | -- | -- |
|
|
| AdventureLog | yes | -- | SECRET_KEY, DB_PASSWORD |
|
|
| Audiobookshelf | yes | yes | -- |
|
|
| BentoPDF | yes | -- | -- |
|
|
| BookStack | yes | -- | APP_KEY, DB_PASSWORD |
|
|
| Cal.com | yes | -- | NEXTAUTH_SECRET, CALENDSO_ENCRYPTION_KEY, DB_PASSWORD |
|
|
| Calibre-Web Automated | yes | yes | -- |
|
|
| Claper | yes | -- | SECRET_KEY_BASE, DB_PASSWORD |
|
|
| Code-Server | yes | -- | PASSWORD |
|
|
| Crafty Controller | yes | -- | -- |
|
|
| Docmost | yes | -- | APP_SECRET, DB_PASSWORD |
|
|
| Emby | yes | yes | -- |
|
|
| FileBrowser Quantum | yes | yes | -- |
|
|
| Ghost | yes | -- | -- |
|
|
| Gitea | yes | -- | -- |
|
|
| Glance | yes | -- | -- |
|
|
| Gokapi | yes | -- | -- |
|
|
| Grafana | yes | -- | GF_SECURITY_ADMIN_PASSWORD |
|
|
| Gramps Web | yes | -- | GRAMPSWEB_SECRET_KEY |
|
|
| Home Assistant | yes | -- | -- |
|
|
| Homebox | yes | -- | -- |
|
|
| Homepage | yes | -- | -- |
|
|
| Immich | yes | yes | DB_PASSWORD |
|
|
| Jellyfin | yes | yes | -- |
|
|
| Kimai | yes | -- | DB_PASSWORD, ADMIN_EMAIL, ADMIN_PASSWORD |
|
|
| Komga | yes | yes | -- |
|
|
| Mealie | yes | -- | -- |
|
|
| n8n | yes | -- | N8N_ENCRYPTION_KEY |
|
|
| Navidrome | yes | yes | -- |
|
|
| Nextcloud | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD |
|
|
| OnlyOffice | yes | -- | JWT_SECRET |
|
|
| OpenGist | yes | -- | -- |
|
|
| Outline | yes | -- | SECRET_KEY, UTILS_SECRET, DB_PASSWORD |
|
|
| Paperless-ngx | yes | yes | PAPERLESS_SECRET_KEY, DB_PASSWORD, PAPERLESS_ADMIN_USER, PAPERLESS_ADMIN_PASSWORD |
|
|
| Papra | yes | -- | -- |
|
|
| Plant-it | yes | -- | JWT_SECRET |
|
|
| Plex | yes | yes | PLEX_CLAIM |
|
|
| PrivateBin | yes | -- | -- |
|
|
| Radarr | yes | yes | -- |
|
|
| Rallly | yes | -- | SECRET_PASSWORD, DB_PASSWORD |
|
|
| RomM | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, ROMM_AUTH_SECRET_KEY |
|
|
| Jellyseerr | yes | -- | -- |
|
|
| Sonarr | yes | yes | -- |
|
|
| SparkyFitness | yes | -- | DB_PASSWORD, APP_DB_PASSWORD, API_ENCRYPTION_KEY, BETTER_AUTH_SECRET |
|
|
| Tandoor Recipes | yes | -- | SECRET_KEY, DB_PASSWORD |
|
|
| Termix | yes | -- | -- |
|
|
| Uptime Kuma | yes | -- | -- |
|
|
| Vaultwarden | yes | -- | ADMIN_TOKEN |
|
|
| Vikunja | yes | -- | VIKUNJA_SERVICE_JWTSECRET |
|
|
| Wanderer | yes | -- | MEILI_MASTER_KEY |
|
|
| wger | yes | -- | SECRET_KEY |
|
|
| Wishlist | yes | -- | -- |
|
|
| Zipline | yes | -- | CORE_SECRET, DB_PASSWORD |
|
|
|
|
### 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)
|
|
|
|
### Docker Compose template standards
|
|
|
|
All templates follow these standards (enforced via audit):
|
|
- `${DOMAIN}` variable syntax for all domain references (not hardcoded)
|
|
- `deploy.resources.limits.memory` on every service container
|
|
- Healthchecks on every service (appropriate for service type)
|
|
- `restart: unless-stopped` on every service
|
|
- `TZ=Europe/Budapest` on every service
|
|
- `traefik-public` external network + internal network for DB services
|
|
- Explicit `container_name:` on every service
|
|
- Header comment block with: app name, domain, DB type, RAM, Pi compatibility
|
|
- `depends_on` with `condition: service_healthy` for DB/Redis dependencies
|
|
|
|
## Legacy: Portainer-based deployments
|
|
|
|
The `generate-customer.sh` script and `templates.json` are kept for Portainer-only setups
|
|
where the felhom-controller is not used. For controller-based deployments, these are not needed.
|
|
|
|
### Legacy workflow
|
|
|
|
```bash
|
|
# Generate customer templates (on your workstation)
|
|
./scripts/generate-customer.sh --customer demo-felhom \
|
|
--domain demo-felhom.eu --hdd-path /mnt/felhom-drives/hdd_1 --push
|
|
```
|
|
|
|
## Adding a New App
|
|
|
|
0. Copy `onboarding/_TEMPLATE.md` to `onboarding/<appname>.md` and answer every check in `NEW-APP-CHECKLIST.md`
|
|
(done with evidence / n/a with a reason). The `onboarding` gate (`scripts/check-onboarding.py`, run by
|
|
`catalog_gates.py --fast`) refuses a new template directory without a complete record; publish both in one commit.
|
|
1. Create `templates/<appname>/docker-compose.yml` following the template standards above
|
|
2. Create `templates/<appname>/.felhom.yml` following the metadata format
|
|
3. Commit and push — the controller will pick it up on next sync
|
|
4. (Legacy) If also needed for Portainer: add entry to `templates.json` and `generate-customer.sh`
|
|
|
|
## Related Repositories
|
|
|
|
| Repository | Purpose |
|
|
|------------|---------|
|
|
| [app-catalog-felhom.eu](https://gitea.dooplex.hu/admin/app-catalog-felhom.eu) | This repo — templates + metadata |
|
|
| [felhom-controller](https://gitea.dooplex.hu/admin/felhom-controller) | felhom-controller + deploy scripts |
|
|
| [felhom.eu](https://gitea.dooplex.hu/admin/felhom.eu) | Website + k3s manifests |
|