admin 7a4ff48590
gates / gates (push) Successful in 2s
wger: run the database migrations at start (R-738)
wger's image runs `manage.py migrate` only when DJANGO_PERFORM_MIGRATIONS=True (entrypoint.sh).
Without it, 2.6 -> 2.7 through the guarded Update on 9202 ended `done` and left 12 migrations
unapplied: the web login answered 500 (no such column: core_userprofile.time_zone). With the switch,
the same step on 9202 ran the migrations and the seeded weight entry read back. No image moves here;
no box reporting to the hub runs wger.

Evidence: felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-30 18:05:48 +02:00

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)

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

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):

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):

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.

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)
# 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

# 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

  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
Repository Purpose
app-catalog-felhom.eu This repo — templates + metadata
felhom-controller felhom-controller + deploy scripts
felhom.eu Website + k3s manifests
S
Description
No description provided
Readme 8.2 MiB
Languages
Python 96.4%
Shell 3.6%