b3eabfd611
wanderer: ghcr.io/flomp/wanderer:0.16.0 is a ghost - upstream split the app into web+db images, moved registry and renamed the org. Restructured to upstream's own v0.20.0 compose (3 services, new /data/plugins volume, second public hostname for PocketBase, meilisearch pinned DOWN to upstream's v1.36.0 per the R-42 ruling). plant-it: retired. The repo name was wrong (plant-it-server) but upstream has DELETED self-hosting; last server image is 2024-12-10 and it needs MySQL+Redis the template never had. Moved to retired/ rather than deleted - reversible. R-41 slice 1: check-image-resolvable.py. Encodes two traps - manifest inspect exits 0 while printing toomanyrequests, and the inverse, where the first sweep called 24 of 65 pins dead because Hub throttled it. Ambiguity is INCONCLUSIVE, never an accusation.
11 KiB
11 KiB
REUSE.md — app-catalog-felhom.eu
Before adding or editing an app, check here. Conventions to copy, traps to avoid. Maintenance: update in the SAME commit that changes a catalog-wide convention. Entries cite real files. Line numbers are landmarks only — reconfirm before editing.
1. Canonical helpers
None — this repo is templates/config, not code. See §2/§5.
2. Canonical patterns (copy structure from THE named file)
| Pattern | Canonical file | Key traits |
|---|---|---|
| The one canonical example app | templates/paperless-ngx/ (both files) |
Multi-container (app + postgres + redis), HDD + userdata mounts, full deploy_fields spectrum (domain/subdomain/secret/password/text/path/select). Copy this structure for any new app. |
.felhom.yml required fields |
templates/paperless-ngx/.felhom.yml |
All 53 apps: display_name, description (Hungarian), category, subdomain, slug, resources{mem_request, mem_limit, pi_compatible, needs_hdd}, deploy_fields, app_info{tagline, use_cases, first_steps, ...}, healthcheck. Optional: smtp_mapping (email-capable apps), open_path (non-root landing page, e.g. ghost). |
| deploy_fields conventions | templates/paperless-ngx/.felhom.yml (deploy_fields: block) |
Every app starts with DOMAIN (type domain) + SUBDOMAIN (type subdomain, locked_after_deploy: true). Secrets: type: secret + generate: — dominant generators password:24 (DB passwords) and hex:32 (app secret keys); password:16 for shown admin passwords (type: password). HDD apps add HDD_PATH (type: path, placeholder /mnt/felhom-drives/hdd_1, locked). Labels/descriptions in Hungarian. |
| Controller-side health probe | templates/vaultwarden/.felhom.yml (healthcheck: block) |
healthcheck.checks[] with type: http (port only), type: api (port + path + expect.status: 200), or type: tcp (port only — mealie, crafty-controller). Prefer api with a real health path when the app has one. |
| Image pinning | ALL templates/*/docker-compose.yml (image: line) |
Never :latest or untagged (recovery-unit ImagePins pins the tag — :latest breaks restore fidelity). Pin a concrete version tag; an app deployed anywhere in the fleet pins to the digest it is RUNNING (pin ≠ upgrade); @sha256: digest pins also count. Gate: python scripts/check-image-pins.py after any compose change (swept 2026-07-12: 5 pins). TRAP: ghcr tags/list can be stale/partial — verify tag existence via docker manifest inspect, never the tag list. |
| Image RESOLVABILITY (does the pin still exist?) | scripts/check-image-resolvable.py + scripts/test_check_image_resolvable.py |
The complement to the pin gate, which is purely syntactic and cannot see rot. Run it at the START of every catalog campaign and before any publish train that vouches the catalog: python3 scripts/check-image-resolvable.py [app …]. Exit 0 all resolve, 1 the registry says an image is GONE, 2 INCONCLUSIVE/harness error. Two traps it encodes, both live-observed: (a) docker manifest inspect prints toomanyrequests and still exits 0 — never trust the exit code alone (same shape as the ISO validate-answer trap); (b) the inverse — the first sweep called 24 of 65 pins dead, postgres:16-alpine among them, because Docker Hub throttled it partway. Ambiguity therefore resolves to INCONCLUSIVE, never to an accusation; a gate that cries wolf gets ignored. Unauthenticated Hub lookups WILL throttle on a full 65-pin sweep — docker login first, or expect exit 2. |
| Docker healthcheck host | ALL templates/*/docker-compose.yml (healthcheck.test:) |
Always 127.0.0.1, never localhost. BusyBox wget (and node/python/curl one-shots) resolve localhost→IPv6 ::1 with NO cross-address-family fallback; an app that binds IPv4-only then reads docker-unhealthy while fully serving (vaultwarden, re-run 2026-07-06 — swept all 48 templates). |
| Docker healthcheck — BusyBox/wget images | templates/vaultwarden/docker-compose.yml (~L49) |
test: ["CMD", "wget", "--spider", "-q", "http://localhost:<port>/<path>"]. Most common family (~20 apps, e.g. homebox, glance). |
| Docker healthcheck — curl-capable images | templates/paperless-ngx/docker-compose.yml (~L76) |
test: ["CMD", "curl", "-f", "http://localhost:<port>/<path>"] (~18 apps: jellyfin, immich, sonarr…). |
| Docker healthcheck — Node images (no wget/curl) | templates/rallly/docker-compose.yml (~L49) |
test: ["CMD", "node", "-e", "require('http').get(...)"] — used when the image lacks wget (that was rallly's actual bug). |
| Docker healthcheck — Python images | templates/mealie/docker-compose.yml (~L47) |
test: ["CMD-SHELL", "python3 -c \"import socket; s=socket.create_connection(('localhost',<port>),2); s.close()\""] (mealie, crafty-controller). tandoor/wger use urllib.request variants for real HTTP checks. |
| Docker healthcheck — DB/Redis sidecars | templates/paperless-ngx/docker-compose.yml (~L107, L129) |
postgres: pg_isready -U <user> -d <db>; mariadb: healthcheck.sh --connect --innodb_initialized; redis: redis-cli ping. App container gets depends_on: <db>: condition: service_healthy. |
| Memory convention | templates/paperless-ngx/docker-compose.yml (~L71) + .felhom.yml resources: |
EVERY service has deploy.resources.limits.memory (compose is the enforcement). NO reservations anywhere. .felhom.yml mem_limit = SUM of all containers' limits (see paperless header comment: 768+256+128=1152M); mem_request = expected steady-state usage, display-only. |
| Compose file skeleton | templates/paperless-ngx/docker-compose.yml (header) |
Header comment (app, domain, DB type, RAM math, Pi), restart: unless-stopped, TZ=Europe/Budapest, explicit container_name, traefik-public external network + <app>-internal for DBs, Traefik labels with Host(`${SUBDOMAIN}.${DOMAIN}`), named volumes for DB/config (NVMe), ${HDD_PATH}/appdata/<app>/... for bulk data, ${USERDATA_PATH}/... for customer-browsable content. |
| App-email (SMTP shim) opt-in | templates/vaultwarden/.felhom.yml (smtp_mapping:) + README.md §smtp_mapping |
smtp_mapping maps shim host/port/security/from to the app's own env names; compose MUST reference the mapped ${VAR:-} keys with empty defaults. STARTTLS if the app can accept self-signed certs, else security_value: "NONE" plaintext (or the :2526 plaintext listener for STARTTLS-insistent clients — see calcom/nextcloud). TRAP: an image that treats defined-but-EMPTY mail vars as "set" (vaultwarden — campaign F1 2026-07-06) needs its own enable-flag gated false in compose and flipped "true" via smtp_mapping.extra; boot-prove a fresh email-off deploy for every new smtp-mapped app. |
| Probe-container naming | templates/vaultwarden/docker-compose.yml (container_name: vaultwarden) + templates/sparkyfitness/ |
The controller-side healthcheck.checks[] probe dials the container whose name equals the stack (directory) name exactly; fallback = the FIRST running prefix-match, which in a multi-container stack can be the DB (verified: felhom-controller/controller/internal/stacks/healthprobe.go findProbeContainer). So the Traefik-exposed service's container_name must be exactly the stack name; sidecars <app>-db, <app>-redis, …. |
3. Dangerous lookalikes — do NOT copy
| Trap | Why it bites | Use instead |
|---|---|---|
templates/gokapi/docker-compose.yml custom entrypoint seeding config.json + --deployment-password |
One-off hack because Gokapi has no env-var headless setup (pinned to ConfigVersion 21 / v1.9.6). Copying this entrypoint pattern to another app will break on image updates. | Normal env-var config via deploy_fields; entrypoint-seeding only as last resort. |
templates.json + scripts/generate-customer.sh |
LEGACY Portainer-only mechanism (marked so in README.md). New apps do NOT need entries here; the controller syncs templates/<app>/ directly. |
Just templates/<app>/{docker-compose.yml,.felhom.yml}. |
templates/uptime-kuma/docker-compose.yml test: ["CMD", "extra/healthcheck"] |
Image-provided binary, unique to this image — not a portable pattern. | Pick the wget/curl/node/python family matching your image (§2). |
4. Seams & interfaces (cross-repo)
- Controller pulls this repo (default 15m) and copies
templates/<app>/docker-compose.yml+.felhom.ymlto/opt/docker/stacks/<app>/; it NEVER overwrites deployedapp.yaml/.env; SHA-256 change detection. Contract description:README.md§"How It Works". .felhom.ymlis the contract surface consumed by felhom-controller (repofelhom-controller/):deploy_fieldsdrive the deploy wizard,resourcesthe deploy screen hints,healthcheck.checksthe controller-side probe,smtp_mappingthe app-email injection at deploy/redeploy.- Assets are NOT in this repo: logo/screenshots resolve from felhom.eu via
slug({assets.base_url}/assets/{slug}-logo.webp— see comment block intemplates/paperless-ngx/.felhom.yml). - Commit+push to main IS the deploy: the controller picks changes up on next sync.
5. Extension points (adding a new app)
templates/<app>/docker-compose.yml— copytemplates/paperless-ngx/docker-compose.ymlskeleton; every service needscontainer_name,restart: unless-stopped,TZ=Europe/Budapest,deploy.resources.limits.memory, a healthcheck (family per §2), Traefik labels on the web service,traefik-publicexternal +<app>-internalnetwork if it has a DB.templates/<app>/.felhom.yml— copytemplates/paperless-ngx/.felhom.yml; required keys per §2; DOMAIN + SUBDOMAIN fields always;mem_limit= sum of compose limits; Hungarian user-facing text;healthcheck.checksprobe.- Update
README.mdApp Catalog + Variable-types tables (convention — every existing app is listed). - Skip
templates.json/generate-customer.sh(legacy, §3). - Email-capable app: add
smtp_mapping+ matching${VAR:-}compose lines (§2 last row).
6. Known inconsistencies (observed — NOT fixed)
README.mdliststemplates/filebrowser/in the tree, but no such directory exists; converselytemplates/recipe-importer/exists but is absent from README's tree and both catalog tables.README.mdfield-type table (§".felhom.yml Format") omitssubdomain(used by all 53 apps) andsecret_input(templates/romm/.felhom.yml), and listsbooleanwhich no descriptor uses.README.mdsayssmtp_mappingis "Currently mapped: Vaultwarden, Mealie" — six descriptors now carry it (calcom, gitea, mealie, nextcloud, rallly, vaultwarden).- Healthcheck URL style drifts:
localhostvs127.0.0.1, trailing-slash vs none,curl -fvscurl -sf, and two distinct Node one-liner styles (compact rallly vs verbose docmost). - One unquoted generator value (
generate: password:24) among otherwise-quotedgenerate: "..."values. - The in-file comment block in
.felhom.ymlheaders (e.g.templates/paperless-ngx/.felhom.yml"Generator types") omitsbase64key:N, whichREADME.mddocuments and one app uses.