Files
app-catalog-felhom.eu/CHANGELOG.md
T
admin 8ddd3c9da5 fix(healthcheck): sweep localhost -> 127.0.0.1 across all 48 templates
BusyBox wget (+ node/python/curl one-shots, incl mealie's socket tuple) resolve
localhost -> IPv6 ::1 with no cross-family fallback; an IPv4-only-binding app
reads docker-unhealthy while serving (vaultwarden, re-run 2026-07-06). Escalates
that instance to the class. Scoped strictly to healthcheck test: lines
(diff-reviewed: no env/config/label changed; .felhom.yml already clean). New
REUSE.md convention row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-06 20:25:54 +02:00

253 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## 2026-07-06 — healthcheck sweep: `localhost` → `127.0.0.1` across all 48 templates
Escalation of the re-run vaultwarden observation
(`felhom.eu/documentation/audits/RERUN-p1p3-2026-07-06.md`) from an instance to a **class**: 48/53
templates used `localhost` in their docker healthcheck `test:` line. BusyBox `wget` (and the node /
python / curl one-shot forms, incl. mealie's `socket.create_connection`) resolve `localhost`→IPv6
`::1` with no cross-address-family fallback, so an IPv4-only-binding app reads docker-`unhealthy`
while fully serving. Mechanical sweep `localhost``127.0.0.1`, scoped strictly to the healthcheck
`test:` lines (diff-reviewed: no app env/config/label line changed; `.felhom.yml` files were already
clean). Industry practice — never `localhost` in container healthchecks. New REUSE.md convention row.
## 2026-07-06 — vaultwarden F1 fix: _ENABLE_SMTP boot-gate (campaign finding, pilot-blocking)
The no-mercy campaign (felhom.eu `audits/CAMPAIGN-nomercy-2026-07-06.md`, finding F1) proved that a
FRESH vaultwarden deploy with app-email off — the default state — crash-loops: the template always
defines `SMTP_HOST=${SMTP_HOST:-}` / `SMTP_FROM=${SMTP_FROM:-}`, and vaultwarden treats a
defined-but-EMPTY env var as "set", so its config validation (`smtp_host.is_some() ==
smtp_from.is_empty()`) errors out and the process exits. The old comment ("empty SMTP_HOST = mail
stays disabled") was wrong for this image. Empirically proven on the pinned
`vaultwarden/server:1.33.2-alpine` (probe P1: defined-empty pair → exact campaign error, exit 12;
P2: `_ENABLE_SMTP=false` + same empty pair → boots; P3: `_ENABLE_SMTP=true` + host+from → boots).
Fix: gate the whole SMTP group with vaultwarden's own `_ENABLE_SMTP` flag — compose default
`false` (validation skipped, mail off, clean boot), flipped to `"true"` by the app-email injection
via `smtp_mapping.extra` (no controller change needed — `extra` already rides `smtpEnv`). The ON
path is byte-identical to the previously send-tested state plus the flag.
Sweep note (no edits): the other five smtp-mapped templates (calcom, gitea, mealie, nextcloud,
rallly) are boot-proven tolerant of defined-empty mail env — all ran healthy as fresh email-off
deploys during the campaign; gitea's `GITEA__mailer__SMTP_ADDR=${...:-}` pattern likewise.
Vaultwarden was the only strict image. New REUSE.md trap row: strict images need an enable-flag
gated `false` in compose + `"true"` in `smtp_mapping.extra`; boot-prove fresh email-off deploys.
## 2026-07-03 — sparkyfitness FINALIZED + live-validated (both VERIFY markers resolved); REUSE probe-naming row
The first worked example of the new `felhom-app-catalog` skill (felhom.eu). Both
`VERIFY-BEFORE-FINALIZE` healthcheck guesses resolved by inspecting the real images on the demo box:
frontend (Alpine/nginx) HAS BusyBox wget → drafted `wget --spider :80/` probe confirmed + kept;
server HAS node v24.17.0 → node-exec `:3010/api/health` probe confirmed (path proven live:
`{"status":"UP"}`). Frontend `container_name` renamed → `sparkyfitness` (= the stack name): the
controller-side probe dials the exact-name container, fallback is the FIRST prefix match (could be
the DB) — new REUSE.md §2 "Probe-container naming" row records the convention (verified in
felhom-controller healthprobe.go). Mem-sum comment added (512+1024+256 = 1792M, value unchanged).
Live-validated on demo via the real dashboard UI (sync + Frissítés): 3/3 containers healthy,
controller probe `healthy: true` (http :80 → 200), `sparky.demo-felhom.eu` 200 via Traefik;
data_key secrets untouched (server/db containers not recreated). Kept deployed.
## 2026-07-03 — docs: CLAUDE.md light expansion
The minimal REUSE-rollout stub expanded to a proper (still ~30-line) CLAUDE.md: what the repo is
(one dir per app, two template files, Hungarian customer text), the push-to-main = deploy contract
(controller sync ≤15 min / manual trigger), legacy `templates.json` warning, and pointers
(REUSE.md, README format spec, the `felhom-build-deploy` skill). No template changes.
## 2026-07-03 — docs: REUSE.md introduced
Cross-repo reuse-map rollout (docs-only). New `REUSE.md`: catalog conventions verified against all
53 apps — the canonical example app (paperless-ngx), `.felhom.yml` required fields, healthcheck
family per image type (BusyBox wget / curl / Node / Python / DB sidecars), memory-limit convention,
new-app checklist, and traps (gokapi entrypoint hack, legacy templates.json). Known README drift
recorded in §6 (NOT fixed). Also a minimal `CLAUDE.md` carrying the REUSE.md pointer + maintenance
rule (full CLAUDE.md is a separate task).
## 2026-06-29 — App-email: calcom + nextcloud (tls_mode=plaintext :2526 + nextcloud split-From)
- **nextcloud** — `smtp_mapping` with `tls_mode: plaintext` (controller injects port 2526, the plaintext-only
listener) + **split From** (`from_var=MAIL_FROM_ADDRESS` + `from_domain_var=MAIL_DOMAIN` → nextcloud@felhom.eu).
Compose references the injected `${SMTP_*}`/`${MAIL_*}`. Live-confirmed: real password-reset delivered via
plaintext :2526 (Symfony Mailer never attempted STARTTLS).
- **calcom** — `smtp_mapping` with `tls_mode: plaintext` (EMAIL_SERVER_HOST/PORT, EMAIL_FROM=calcom@felhom.eu).
**Plus three pre-existing template fixes** (calcom never deployed before — the image pin was invalid):
(1) image `v4.8.7``v4.6.9` (the pinned tag has no published image); (2) added required `DATABASE_DIRECT_URL`
(Prisma `migrate deploy` fails without it → incomplete schema → 500s); (3) healthcheck `/api/health`
`/api/auth/providers` (the old path 404s in v4.x → container stayed unhealthy → Traefik wouldn't route).
- Both apps point at the controller's `:2526` plaintext-only listener because their SMTP clients
opportunistically STARTTLS-upgrade and can't skip the self-signed cert — the listener simply doesn't offer
STARTTLS, so they stay plaintext (accepted on the single-tenant app bridge).
## 2026-06-29 — App-email rollout: gitea + rallly (calcom/nextcloud/immich = findings)
- **gitea 1.23.4** — added `smtp_mapping` (STARTTLS via `GITEA__mailer__PROTOCOL=smtp+starttls` +
`FORCE_TRUST_SERVER_CERT=true` to trust the shim's self-signed cert; single `GITEA__mailer__FROM`). Compose
references the injected `GITEA__mailer__*` keys; env applied every boot.
- **rallly** — added `smtp_mapping` (Nodemailer STARTTLS, `SMTP_SECURE=false` + `SMTP_REJECT_UNAUTHORIZED=false`
to accept the self-signed cert; single `NOREPLY_EMAIL`). **Also fixed three pre-existing template bugs** that
made rallly undeployable (never caught because the bad pin never ran): (1) image pin `3.12.1` doesn't exist →
`3.11.2`; (2) healthcheck used `wget`, absent from the rallly image (exit 127) → container unhealthy →
**Traefik wouldn't route it** → replaced with a Node http check; (3) added required `SUPPORT_EMAIL` + a valid
`NOREPLY_EMAIL` default (rallly refuses to boot without them).
- **Both gitea and rallly send-tested live** end-to-end (app → shim → hub → Resend): gitea password-reset
(From `gitea@felhom.eu`) and rallly registration code (From `rallly@felhom.eu`) both delivered.
- **NOT wired — reported as findings** (`felhom.eu/documentation/audits/FINDING-app-email-rollout-2026-06-29.md`):
- **cal.com v4.8.7** — hard-codes TLS `rejectUnauthorized:true` with no override; opportunistic STARTTLS
against the self-signed shim fails. Needs a non-STARTTLS-advertising plaintext listener (mechanism change).
- **nextcloud 31** — no cert-skip env (same opportunistic-STARTTLS gap) **and** a split From
(`MAIL_FROM_ADDRESS`+`MAIL_DOMAIN`) the single-`from_var` mapping can't express.
- **immich v2.5.5** — no SMTP env vars at all; config is admin-UI/DB or an `IMMICH_CONFIG_FILE` JSON. Does not
fit env-injection; left for a future config-file-injection mechanism (or manual admin-UI setup).
## 2026-06-29 — App-email: smtp_mapping for Vaultwarden + Mealie
- Added the `smtp_mapping` block to `templates/vaultwarden/.felhom.yml` and `templates/mealie/.felhom.yml`,
enabling managed outbound email (app → in-controller shim → hub → Resend) for the two spike-proven apps
(`SPIKE-smtp-app-relay-2026-06-28`). The controller injects `SMTP_*` at deploy/redeploy when app-email is
on (global + per-app); the From address is `<app>@felhom.eu`. SMTP auth creds are intentionally left unset
(the shim accepts no-auth on the Docker network).
- **Vaultwarden:** STARTTLS (`SMTP_SECURITY=starttls`) + `SMTP_ACCEPT_INVALID_CERTS/HOSTNAMES=true` to
accept the shim's self-signed cert.
- **Mealie:** plaintext (`SMTP_AUTH_STRATEGY=NONE`) on :2525 — Mealie has no accept-invalid-cert option, so
STARTTLS to a self-signed shim would fail; plaintext to the Docker-network-only shim is the spike-validated
mode.
- Both `docker-compose.yml` files now reference the injected `${SMTP_*}` keys (with harmless defaults) so the
values reach the container; empty `SMTP_HOST` keeps mail disabled when the toggle is off.
- Documented the `smtp_mapping` pattern in `README.md` so further apps are easy adds.
## 2026-06-28 — Add SparkyFitness (v0.17.2) — nutrition/workout tracker
- New app `templates/sparkyfitness/{docker-compose.yml,.felhom.yml}`: a self-hosted nutrition/calorie +
workout/weight tracker (alternative to wger). Three containers — nginx **frontend** (SPA :80, the sole
Traefik ingress, proxies `/api`+`/uploads` internally) + Node **server** (:3010) + dedicated
**postgres:15-alpine**. Server + DB stay on the internal network with no Traefik labels.
- **Native email/password auth** (no OIDC/Authentik — that's DooPlex-specific); subdomain `sparky`
(deliberately ≠ wger's `fitness` to avoid a Host() collision). `pi_compatible: false`, `needs_hdd: false`.
- **Two DB roles**: `sparky` (POSTGRES superuser, runs init/migrations) + `sparkyapp` (limited app role the
server auto-creates on first boot) — separate `DB_PASSWORD`/`APP_DB_PASSWORD`. `PGDATA` in a `pgdata`
subdir of the named volume. Four auto-generated, `locked_after_deploy` secrets; `API_ENCRYPTION_KEY` +
`BETTER_AUTH_SECRET` carry `data_key: true` (restore recovers, never regenerates — both are 64-char hex).
- Transcribed from the validated k3s manifest `homelab-manifests/workout-system/sparkyfitness.yaml`
(pinned image tags, two-DB-role model, never-change crypto keys, `/api/health`, pg15 + PGDATA subdir).
- **Image-probe findings (build server, v0.17.2):** server keeps the `node -e` `/api/health` probe (node
present); frontend keeps the `wget --spider` probe (both `wget` and `curl` present). No probe changes needed.
- **Live-validated on guest 9201 (controller v0.87.0):** synced via "Sablonok frissítése"; deployed through
the real dashboard flow (Domain auto, Subdomain `sparky`, 4 secrets auto-gen). All 3 containers healthy;
server log shows clean migrations + `sparkyapp` role created + RLS applied, no crash loop, no uploads
EACCES; `GET /api/health` through the public edge returns `{"status":"UP"}`; login/register page serves
over a valid TLS cert at `https://sparky.demo-felhom.eu`.
## 2026-06-26 — crafty-controller: image bump 4.4.8→4.10.7 + publish Java port range + connection guidance
- **Image bump** `crafty-4:4.4.8``4.10.7` (latest stable; 4.10.8/4.11.0 don't exist in the registry).
6 minor versions of fixes incl. security CVEs. **Java 25 verified present** in 4.10.7
(`/usr/lib/jvm/java-25-openjdk-amd64`, default `java -version` = openjdk 25.0.3; 8/11/17/21 also
available) — so the latest-Minecraft (`26.x`, needs Java 25) blocker is resolved. Healthcheck + Traefik
https-backend labels unchanged (Crafty still serves HTTPS on 8443).
- **Published the Java game-port range** `25565-25575:25565-25575` (TCP, 11 ports = up to 11 Java
servers; first server 25565, rest 2556625575). No `network_mode: host` (would break Traefik routing).
Bedrock UDP 19132 intentionally out of scope.
- **App-page guidance** (`.felhom.yml` first_steps + prerequisites): how to set the server port within
2556525575, how to connect on the LAN (manual IP:port — "scan for LAN" won't auto-list), and that
internet access needs operator port-forwarding. (Static text — can't show the live LAN IP.)
- **Live-verified on guest 9201:** 4.10.7 healthy; public URL 302; the guest's bridged LAN IP
`192.168.0.121` reaches the real Crafty "test" server on `25565` (TCP OPEN + Minecraft SLP handshake
returns JSON status); `:25575` reachable, `:25600` closed (negative control). In-place upgrade preserved
the admin, the operator's configured MFA, and the test server.
- **Correction (earlier draft was wrong):** an earlier note here claimed the upgrade "locked out the
admin (TOTP)." That was a misdiagnosis — the `totp_data` row + recovery codes were **operator-configured
MFA**, so the 401 on a password-only login was correct behaviour, NOT an upgrade bug. There is **no
upgrade regression**; the bump preserves data and MFA correctly.
## 2026-06-26 — crafty-controller: seed a felhom-generated admin password (replaces Crafty's ugly random one)
- **crafty-controller**: instead of reading Crafty's auto-generated (long, symbol-laden) random admin
password, we now **seed** a clean felhom-generated one — same pattern as gokapi, so initial passwords are
consistent across the catalog.
- Crafty's image ships `app/config_original/default.json = {"username":"admin","password":"crafty"}`;
"crafty" is 6 chars < Crafty's 8-char minimum, so Crafty rejected it and generated a random password.
- New `CRAFTY_PASSWORD` deploy field (`type: password`, `generate: password:24`, locked after deploy —
mirrors gokapi's `GOKAPI_PASSWORD`). The compose **entrypoint** overwrites the `default.json` template
with this password before the launcher runs; on fresh install Crafty creates the `admin` user with it.
- `initial_credentials.file` repointed `default-creds.txt``default.json` (same json/username/password
keys), so the controller's app-page "Kezdeti belépési adatok" card shows the **seeded** password — the
customer sees the same value at deploy time and on the app page.
- Catalog-only change (reuses felhom-controller v0.84.0's initial_credentials reader + the gokapi-style
seed). Requires a fresh install to take effect (the seed is only read on first run).
## 2026-06-26 — crafty-controller: surface the auto-generated initial admin password on the app page
- **crafty-controller**: Crafty writes a random admin password to `/crafty/app/config/default-creds.txt`
at first boot (its built-in default is rejected as "too short"). Customers had to read the container
logs to find it. Added an `initial_credentials` block (new general felhom-controller v0.84.0 mechanism):
`file` + `format: json` + `username_key`/`password_key` + a `note`. The controller reads the file live
from the container and shows username + password (masked, reveal/copy) on the app's page under "Kezdeti
belépési adatok". Requires felhom-controller ≥ v0.84.0.
- Updated `first_steps` to point at the app page for the initial login instead of "find it in the logs".
## 2026-06-26 — crafty-controller: Traefik https backend + scoped skip-verify (fixes 502)
- **crafty-controller**: the healthcheck fix un-withheld the Traefik route, exposing a pre-existing
**502** — Traefik proxied `http://…:8443` to Crafty's **HTTPS-only** self-signed backend (Crafty serves
no plain-HTTP panel; `:8000` only redirects). Added two service labels:
- `loadbalancer.server.scheme=https` — Traefik now speaks HTTPS to the backend.
- `loadbalancer.serverstransport=insecure-skip-verify@file` — references the **named**
serversTransport defined in the controller-managed Traefik dynamic config (felhom-controller v0.83.0),
which skips verifying Crafty's per-container self-signed cert. Verification stays ON for every other
backend (scoped Option B; no global `insecureSkipVerify`). The `@file` suffix is the cross-provider
reference from the docker provider to the file-provider transport.
- Requires felhom-controller ≥ v0.83.0 (which renders the `insecure-skip-verify` transport). `port=8443`
and the router/tls labels are unchanged.
## 2026-06-26 — crafty-controller healthcheck fix (curl-absent + http-vs-TLS probe)
- **crafty-controller**: container was permanently `unhealthy` → route withheld (`routeUnpublished`).
Two independent healthcheck root causes, both fixed in one change:
- **Docker healthcheck** ran `curl -fk https://localhost:8443`, but the `crafty-4:4.4.8` image has
**no `curl` and no `wget`** (`exec: "curl": not found`, FailingStreak 150). Replaced with a
dependency-free **python3 TLS-socket** liveness probe (`/usr/bin/python3` is present): completes a
TLS handshake to `127.0.0.1:8443` (unverified context mirrors the old `-k`; Crafty's cert is
self-signed). `start_period` 30s → 60s for cold-boot headroom (cert gen + migrations).
- **Controller-side probe** (`.felhom.yml healthcheck.checks`) was `type: http` against Crafty's
**TLS-only** 8443 → `probeHTTP` sent plaintext HTTP, got a TLS record → `HealthProbe.Healthy=false`,
which `manager.go` re-applies to override Docker's verdict back to `unhealthy`. Changed `http`
`tcp` (`probeTCP` dial succeeds against a TLS listener). Both layers had to change together.
- Live-validated on guest 9201 (`demo-felhom`): synced → recreated via the update path → Docker
`State.Health: healthy` (ExitCode 0), `health_probe.healthy: true` (tcp :8443, 5ms), http-vs-TLS
WARNs stopped, stable green 3+ min, Traefik now **publishes** the route (`crafty-controller@docker`).
- **Known follow-up (separate, out of this fix's scope):** the public URL still returns **502** — a
distinct pre-existing bug the un-withheld route exposed: Traefik proxies `http://…:8443` to Crafty's
HTTPS-only backend. Needs a Traefik HTTPS-backend + self-signed `serversTransport`
(`insecureSkipVerify`) in the controller-generated Traefik config — tracked separately.
## 2026-06-23 — gokapi: index redirect + admin username display
- **gokapi**: seed `RedirectUrl` repointed from Gokapi's GitHub default → `https://${SUBDOMAIN}.${DOMAIN}/admin`.
Gokapi's bare root `/` redirects to `RedirectUrl`; the controller's "Megnyitás" link is always the bare
subdomain root, so it was landing on Gokapi's GitHub instead of the app. Now `/``/admin` → login.
Applied to the live demo (config.json edit + restart) and the seed (future deploys).
- **gokapi**: added `app_info.default_creds` ("Felhasználó: admin · jelszó a Beállítások oldalon") so the
app-info page shows the initial admin user like other apps; fixed `first_steps` (no more setup wizard).
## 2026-06-23 — gokapi reproducible headless setup (fixes public "maintenance mode")
- **gokapi**: was stuck in "maintenance mode" on the public URL since first deploy — Gokapi's one-time
`/setup` wizard was never completed, and (verified against the docs + the v1.9.6 binary) **no Gokapi
version supports env-var headless setup** for admin credentials. Worse, the unconfigured `/setup` was
publicly reachable = an unauthenticated admin-takeover window.
- Fix: the compose `entrypoint` now seeds a `config.json` on first boot (admin user, this app's public
URL `https://${SUBDOMAIN}.${DOMAIN}/`, local storage, Encryption Level 0 so it restarts without a
prompt) with `Password`/`SaltAdmin`/`SaltFiles` cleared, then runs Gokapi's documented
`--deployment-password` one-shot to set the **felhom-generated** admin password **before** the
server starts serving. The admin account is claimed at first boot → `/setup` is never exposed.
- `.felhom.yml`: new `GOKAPI_PASSWORD` deploy field (`type: password`, `generate: password:24`,
shown to the customer, locked after deploy). Admin username is `admin`.
- Seed is pinned to Gokapi **v1.9.6** (`ConfigVersion 21`) — re-capture the seed if the image is bumped.
- Live-validated on guest 9201: fresh remove+redeploy → headless auto-config, public login works, no
maintenance page, admin claimed at first boot (browser-verified login).
## 2026-06-22 — gitea healthcheck fix (unattended test campaign)
- **gitea**: healthcheck probe repointed `/api/v1/version``/api/healthz` (docker HC + controller
`.felhom.yml` probe), `start_period` 30s → 90s.
- Surfaced during the Phase-2 deploy sweep: a fresh gitea reported `unhealthy` because
`/api/v1/version` returns 404 until the install wizard / INSTALL_LOCK completes, while the
container was serving fine on :3000 (`/api/healthz` → 200). Same class as the komga fix.
## 2026-06-22 — komga healthcheck fix (unattended test campaign)
- **komga**: healthcheck probe repointed `/api/v1/actuator/health``/actuator/health`.
- Root cause: komga's Spring Boot actuator endpoint is served unauthenticated at `/actuator/health`
(HTTP 200), while everything under the `/api/v1` prefix is auth-gated — so the old probe got
HTTP 401, `curl -f` exited 22, and the container reported `unhealthy` despite serving normally
on :25600. Diagnosed live on guest 9201 (probe matrix: `/`, `/actuator/health`,
`/api/v1/oauth2/providers`, `/login` all 200; `/api/v1/actuator/health` → 401).
- The `gotson/komga:1.20.0` image ships `curl` (verified), so the probe tool is unchanged.