fix(vaultwarden): _ENABLE_SMTP boot-gate — fresh email-off deploys crash-looped (campaign F1)
Vaultwarden treats defined-but-empty SMTP_HOST/SMTP_FROM as "set"; with upstream _enable_smtp defaulting true its validation errors out and the container crash-loops on every fresh deploy with app-email off (the default). Gate the SMTP group with _ENABLE_SMTP: compose default false (clean boot, mail off), flipped "true" by the app-email injection via smtp_mapping.extra. Proven on the pinned 1.33.2-alpine image (P1 repro exit 12 / P2 off boots / P3 on boots). REUSE.md: strict-image TRAP row. Sweep note: the other 5 smtp-mapped templates are campaign-boot-proven tolerant; no edits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## 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
|
The first worked example of the new `felhom-app-catalog` skill (felhom.eu). Both
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
# REPORT — sparkyfitness finalized: image-inspected healthchecks + probe-container naming (2026-07-03)
|
# REPORT — vaultwarden F1 fix: `_ENABLE_SMTP` boot-gate (2026-07-06)
|
||||||
|
|
||||||
**Class:** template finalization + live validation (no version scheme; push = deploy).
|
**Class:** implementation (campaign-finding fix; push = deploy).
|
||||||
**Baseline:** `main` @ `15cdf7b` → **`c7490f7`**. Companion: the NEW `felhom-app-catalog` skill
|
**Baseline:** `main` @ `13eedb1` → (this commit). Provenance: felhom.eu
|
||||||
(felhom.eu — this task was its first worked example; see that repo's REPORT for the skill half).
|
`documentation/audits/CAMPAIGN-nomercy-2026-07-06.md` finding **F1** (pilot-blocking: every fresh
|
||||||
|
vaultwarden deploy with app-email off — the default — crash-loops).
|
||||||
|
|
||||||
## What changed
|
## Corrected root cause (the campaign report's hypothesis was wrong)
|
||||||
|
|
||||||
- `templates/sparkyfitness/docker-compose.yml`:
|
Not "shim sets SMTP_FROM without SMTP_HOST" — with app-email off the shim injects **nothing**. The
|
||||||
- Both `VERIFY-BEFORE-FINALIZE` markers RESOLVED by inspecting the actual images on the demo box
|
crasher: the template always defines `SMTP_HOST=${SMTP_HOST:-}` and `SMTP_FROM=${SMTP_FROM:-}`;
|
||||||
(the skill's core rule — never guess):
|
vaultwarden's `get_env_str_value` returns `Some("")` for a defined-empty var (**empty counts as
|
||||||
- **frontend** `codewithcj/sparkyfitness:v0.17.2` (Alpine/nginx): has `/usr/bin/wget` (and
|
set**), and with `_enable_smtp` defaulting `true` upstream, its validation
|
||||||
curl; no node/python3) → the drafted BusyBox-wget `--spider :80/` probe kept, marker
|
`smtp_host.is_some() == smtp_from.is_empty()` → `true == true` → config error → exit → crash-loop.
|
||||||
replaced with the inspection provenance.
|
The old template comment ("empty SMTP_HOST = mail stays disabled") was wrong for this image.
|
||||||
- **server** `codewithcj/sparkyfitness_server:v0.17.2`: has node v24.17.0 (and wget+curl) →
|
|
||||||
node-exec GET `:3010/api/health` kept (checks the real status, not mere reachability);
|
|
||||||
`/api/health` proven live: `{"status":"UP"}`.
|
|
||||||
- **Frontend `container_name` renamed `sparkyfitness-frontend` → `sparkyfitness`**: the
|
|
||||||
controller-side probe dials the container named exactly like the stack (fallback = FIRST
|
|
||||||
prefix match — could be the DB). New REUSE.md §2 row records the convention.
|
|
||||||
- Both image tags reconfirmed to resolve (`docker manifest inspect`).
|
|
||||||
- `templates/sparkyfitness/.felhom.yml`: mem-sum arithmetic comment added
|
|
||||||
(db 512 + server 1024 + frontend 256 = **1792M** — matches the existing value; nothing corrected).
|
|
||||||
- `REUSE.md` §2: NEW "Probe-container naming" row (verified against
|
|
||||||
`felhom-controller/internal/stacks/healthprobe.go` `findProbeContainer`).
|
|
||||||
|
|
||||||
## Live validation (demo guest 9201; method stated per claim)
|
## Empirical probe (mandatory gate, run on 180 against the pinned image BEFORE editing)
|
||||||
|
|
||||||
SparkyFitness was already running from the draft (deployed 4 h earlier, 3× healthy). Applying the
|
| Probe | Env | Result |
|
||||||
finalized template used the REAL dashboard UI via the browser bridge: "Sablonok frissítése" (sync
|
|---|---|---|
|
||||||
confirmed by the guest's stacks file gaining the rename, zero VERIFY markers), then the card's
|
| P1 (pre-fix shape) | `SMTP_HOST=` `SMTP_FROM=` (defined-empty) | **exit 12** — *"Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`"* (exact campaign error) — the image-level red-proof |
|
||||||
"Frissítés". Result:
|
| P2 (fix, OFF) | + `_ENABLE_SMTP=false` | **boots** — "Rocket has launched", running |
|
||||||
|
| P3 (fix, ON) | `_ENABLE_SMTP=true` + host + from | **boots** — running (no SMTP connect at boot) |
|
||||||
|
|
||||||
- Only the renamed frontend was recreated — `sparkyfitness … Up 21 seconds (healthy)`; server + db
|
(P2/P3 needed `I_REALLY_WANT_VOLATILE_STORAGE=true` to bypass the unrelated no-volume guard in the
|
||||||
stayed `Up 4 hours` → **the two `data_key` secrets (API_ENCRYPTION_KEY, BETTER_AUTH_SECRET) were
|
bare `docker run`; P1 control re-run with it still crashes on the SMTP error, isolating the cause.)
|
||||||
NOT regenerated** (deployed app.yaml/.env untouched, exactly the data_key contract).
|
Logs: DooPlex `~/campaign-nomercy-2026-07-05/evidence/PROBE-F1/`.
|
||||||
- Docker health: 3/3 healthy. Controller-side probe (via `GET /api/stacks/sparkyfitness`, the same
|
|
||||||
data the UI polls): `healthy: true — http :80/ → 200 (6ms)` against the renamed container.
|
|
||||||
- `sparky.demo-felhom.eu` answers **200** via Traefik in-guest (Host-header curl).
|
|
||||||
- **Kept deployed** on demo (7.3 option A — expendable box, real catalog app). The data_key
|
|
||||||
secrets exist only in the deployed app.yaml, never committed.
|
|
||||||
|
|
||||||
## Notes / observations (not acted on)
|
## Changes
|
||||||
|
|
||||||
- Pre-rename, the controller probe was ALSO green — the prefix fallback happened to pick the
|
- `templates/vaultwarden/docker-compose.yml` — `_ENABLE_SMTP=${_ENABLE_SMTP:-false}` added to the
|
||||||
frontend. The rename converts luck into the deterministic convention.
|
SMTP group; the wrong comment replaced with the empty-counts-as-set TRAP note (+ config.json
|
||||||
- `paperless-ngx`'s container names match neither the exact nor the prefix rule of its stack name —
|
admin-panel precedence note). Nothing else touched.
|
||||||
its controller-side probe likely never runs. Candidate follow-up, not touched.
|
- `templates/vaultwarden/.felhom.yml` — `smtp_mapping.extra: _ENABLE_SMTP: "true"` (rides the
|
||||||
- BusyBox `ash` `command -v` silently ignores all but its first argument — inspect one tool per
|
existing `smtpEnv` extra loop; zero controller change).
|
||||||
run (now in the skill).
|
- `REUSE.md` — TRAP appended to the App-email row: strict images need the enable-flag pattern;
|
||||||
- App-count drift recorded in the felhom.eu REPORT (53 templates vs website "45+" / category sum 49).
|
boot-prove fresh email-off deploys for every new smtp-mapped app.
|
||||||
|
- `CHANGELOG.md` — entry incl. the sweep note.
|
||||||
|
|
||||||
|
## Sweep note (documentation only, no edits)
|
||||||
|
|
||||||
|
calcom, gitea, mealie, nextcloud, rallly (and gitea's `GITEA__mailer__SMTP_ADDR=${...:-}`) are
|
||||||
|
**boot-proven tolerant** of defined-empty mail env — all ran healthy as fresh email-off deploys in
|
||||||
|
the campaign. Vaultwarden was the only strict image.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Probe gate P1/P2/P3 above (image level).
|
||||||
|
- Live pipeline validation (Scenario A fresh email-off deploy healthy; Scenario B toggle ON→OFF
|
||||||
|
regression) executed as part of the controller v0.101.0 deployment — see
|
||||||
|
felhom-controller/REPORT.md for the live evidence.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ None — this repo is templates/config, not code. See §2/§5.
|
|||||||
| 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`. |
|
| 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. |
|
| 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. |
|
| 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). |
|
| 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/internal/stacks/healthprobe.go` `findProbeContainer`). So the Traefik-exposed service's `container_name` must be exactly the stack name; sidecars `<app>-db`, `<app>-redis`, …. |
|
| 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/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
|
## 3. Dangerous lookalikes — do NOT copy
|
||||||
|
|||||||
@@ -102,3 +102,7 @@ smtp_mapping:
|
|||||||
extra:
|
extra:
|
||||||
SMTP_ACCEPT_INVALID_CERTS: "true"
|
SMTP_ACCEPT_INVALID_CERTS: "true"
|
||||||
SMTP_ACCEPT_INVALID_HOSTNAMES: "true"
|
SMTP_ACCEPT_INVALID_HOSTNAMES: "true"
|
||||||
|
# Boot-gate for Vaultwarden's strict SMTP validation (campaign finding F1, 2026-07-06):
|
||||||
|
# the image errors out when SMTP_HOST/SMTP_FROM are defined-but-empty, so the compose
|
||||||
|
# default is _ENABLE_SMTP=false and this injection flips it on with the rest of the group.
|
||||||
|
_ENABLE_SMTP: "true"
|
||||||
|
|||||||
@@ -29,7 +29,14 @@ services:
|
|||||||
- WEBSOCKET_ENABLED=true
|
- WEBSOCKET_ENABLED=true
|
||||||
- TZ=Europe/Budapest
|
- TZ=Europe/Budapest
|
||||||
# App-email (managed relay). Injected by the controller only when app-email is on
|
# App-email (managed relay). Injected by the controller only when app-email is on
|
||||||
# (global + per-app); empty SMTP_HOST = Vaultwarden mail stays disabled. See .felhom.yml smtp_mapping.
|
# (global + per-app); see .felhom.yml smtp_mapping.
|
||||||
|
# TRAP (campaign finding F1, 2026-07-06): Vaultwarden treats a defined-but-EMPTY env var as
|
||||||
|
# "set" — with SMTP_HOST/SMTP_FROM both defined-empty its config validation errors out and
|
||||||
|
# the container crash-loops. The whole SMTP group is therefore gated by _ENABLE_SMTP
|
||||||
|
# (default false = validation skipped, mail off); the controller's app-email injection flips
|
||||||
|
# it to true via smtp_mapping.extra. Note: a config.json saved from the admin panel would
|
||||||
|
# override these env values — not applicable to fresh deploys.
|
||||||
|
- _ENABLE_SMTP=${_ENABLE_SMTP:-false}
|
||||||
- SMTP_HOST=${SMTP_HOST:-}
|
- SMTP_HOST=${SMTP_HOST:-}
|
||||||
- SMTP_PORT=${SMTP_PORT:-587}
|
- SMTP_PORT=${SMTP_PORT:-587}
|
||||||
- SMTP_SECURITY=${SMTP_SECURITY:-off}
|
- SMTP_SECURITY=${SMTP_SECURITY:-off}
|
||||||
|
|||||||
Reference in New Issue
Block a user