From 139d40b0fdf7e5c51eef2373c14ee0fcdb6714d5 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 12 Jun 2026 03:03:18 -0700 Subject: [PATCH 1/2] docs(web/guides): fix deployment guides to match verified wheels deploy behavior Audit batch 2 (p1-16-deploy) corrections across seven deployment guides: broken invocations replaced with the working flat aliases, env.secret delivery marked pending (#2956/#2957), setup==deploy orchestration gap called out (#2957), failure-hook masking documented (#3087), accessory files:/labels corrected (#3088), and all four broken observability examples fixed against a live Lucee 7 harness. Co-Authored-By: Claude Fable 5 Signed-off-by: Peter Amiri --- .../docs/v4-0-0/deployment/accessories.mdx | 22 ++++--- .../docs/v4-0-0/deployment/first-deploy.mdx | 50 ++++++++++----- .../content/docs/v4-0-0/deployment/hooks.mdx | 16 ++--- .../deployment/migrating-from-kamal.mdx | 31 +++++----- .../deployment/observability-and-logging.mdx | 39 +++++++++--- .../docs/v4-0-0/deployment/secrets.mdx | 62 ++++++++++--------- .../docs/v4-0-0/deployment/vm-deployment.mdx | 6 +- 7 files changed, 137 insertions(+), 89 deletions(-) diff --git a/web/sites/guides/src/content/docs/v4-0-0/deployment/accessories.mdx b/web/sites/guides/src/content/docs/v4-0-0/deployment/accessories.mdx index 7e5adb34bf..f22165499e 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/deployment/accessories.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/deployment/accessories.mdx @@ -49,7 +49,7 @@ accessories: `wheels deploy accessory boot redis` on first deploy. Produces a container named `-redis` on the named host, published on 6379. From the app side, connect to `redis://192.0.2.20:6379`. -## Postgres with volume, env, and an init file +## Postgres with volume and env ```yaml title="config/deploy.yml (illustrative — do not type)" accessories: @@ -61,21 +61,23 @@ accessories: clear: POSTGRES_USER: app POSTGRES_DB: myapp_production - secret: - - POSTGRES_PASSWORD volumes: - /data/pg:/var/lib/postgresql/data - files: - - config/init.sql:/docker-entrypoint-initdb.d/init.sql ``` -- `env.secret:` pulls `POSTGRES_PASSWORD` from `.kamal/secrets` — never commit it to `deploy.yml`. +- `env.clear:` values become `docker run -e` flags on the accessory container. - `volumes:` persists `/var/lib/postgresql/data` to the host so the database survives `docker rm`. -- `files:` uploads local paths to the container filesystem at deploy time. Useful for initialization scripts, client certs, or any read-only payload the container needs. + + ## Named containers and labels -Accessory containers are named `-` — the example above yields `myapp-db` and `myapp-redis`. They carry the same `service=` label as your app containers, so `wheels deploy details` lists them alongside everything else. +Accessory containers are named `-` — the example above yields `myapp-db` and `myapp-redis`. Their `service=` label uses that same combined value (`service=myapp-db`), **not** the app containers' bare `service=myapp` — so a `docker ps --filter label=service=myapp` won't catch them. `wheels deploy details` still lists them alongside everything else because it inspects each declared accessory container by name rather than relying on the shared label. ## Multi-host accessories @@ -114,7 +116,7 @@ wheels deploy accessory boot all wheels deploy accessory stop all ``` -`wheels deploy setup` boots every declared accessory as part of first-run. `wheels deploy remove` tears them down. +`wheels deploy setup` does **not** boot accessories in the current Phase 1 CLI — it's an alias for `wheels deploy`, and full first-run orchestration is tracked in [#2957](https://github.com/wheels-dev/wheels/issues/2957). Run `wheels deploy accessory boot all` explicitly as part of first-run. `wheels deploy remove` does tear them down. ## Accessories and `wheels deploy` @@ -133,6 +135,6 @@ When you do want to change an accessory — a Redis version bump, a Postgres con diff --git a/web/sites/guides/src/content/docs/v4-0-0/deployment/first-deploy.mdx b/web/sites/guides/src/content/docs/v4-0-0/deployment/first-deploy.mdx index db97ee3bc9..c15d4f73c3 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/deployment/first-deploy.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/deployment/first-deploy.mdx @@ -18,7 +18,7 @@ This page takes an existing Wheels app and ships it to one or more Linux servers - How to verify the rollout, tail logs, and roll back if something breaks ## Before you start @@ -27,7 +27,7 @@ Three quick checks before running anything: - **SSH works.** `ssh deploy@your-host "uname -a"` succeeds without a password prompt. - **Registry login works.** `docker login ` succeeds locally. -- **Your app has a `Dockerfile`.** It builds and runs locally (`docker build . && docker run `). If not, write the `Dockerfile` first — `wheels deploy` doesn't generate one. +- **Your app has a working `Dockerfile`.** It builds and runs locally (`docker build . && docker run `). If you don't have one yet, `wheels deploy init` generates a starter `Dockerfile` in step 1 (and refuses to overwrite an existing one unless you pass `--force`) — but verify the generated one actually builds your app before deploying. ## Walk-through @@ -41,10 +41,12 @@ Three quick checks before running anything: wheels deploy init ``` - Creates two files: + Creates four files: - `config/deploy.yml` — the deploy manifest. Under git. - `.kamal/secrets` — secret values. **Never under git.** Add `.kamal/secrets` to `.gitignore` immediately. + - `Dockerfile` — a starter production Dockerfile. If one already exists, `init` refuses to overwrite it unless you pass `--force`. + - `.dockerignore` — keeps build context lean. A `.kamal/hooks/` directory is also created for optional local hook scripts. It's empty and safe to leave that way. @@ -88,7 +90,8 @@ Three quick checks before running anything: # Registry — matches registry.password[0] in deploy.yml KAMAL_REGISTRY_PASSWORD=$(op read op://Production/Registry/password) - # App-level secrets — referenced under env.secret in deploy.yml + # App-level secrets — will feed env.secret once container delivery + # lands (#2956/#2957); harmless to stage here in the meantime DATABASE_URL=$(op read op://Production/App/database-url) WHEELS_RELOAD_PASSWORD=$(op read op://Production/App/reload-password) ``` @@ -106,30 +109,43 @@ Three quick checks before running anything: clear: WHEELS_ENV: production WHEELS_DATASOURCE_CLASS: com.mysql.cj.jdbc.Driver - secret: - - DATABASE_URL - - WHEELS_RELOAD_PASSWORD ``` - `clear:` values are baked into `deploy.yml` and safe under git. `secret:` names pull from `.kamal/secrets`. At deploy time, `wheels deploy` translates both into `docker run -e` flags. + `clear:` values are baked into `deploy.yml` and safe under git. At deploy time, `wheels deploy` translates them into `docker run -e` flags. + + 5. **Bootstrap Docker on the host (first time only).** If Docker is already installed on the target, skip this step. Otherwise: - ```bash {test:cli cmd="wheels deploy server bootstrap"} - wheels deploy server bootstrap + ```bash title="Install Docker on every host (requires reachable hosts)" + wheels deploy bootstrap ``` Runs `which docker || curl -fsSL https://get.docker.com | sh` on every host. Idempotent — safe to run on a host that already has Docker. Fails fast if SSH or `sudo` isn't configured correctly. + + 6. **Run setup once.** ```bash title="First-time deploy" wheels deploy setup ``` - `setup` is the first-run verb. It runs the full deploy flow *and* boots `kamal-proxy` and any accessories you've declared. On subsequent deploys you use `wheels deploy` (without `setup`) because the proxy and accessories are already running. + `setup` is the first-run verb. In the current Phase 1 CLI it is an alias for `wheels deploy` — it does **not** yet boot `kamal-proxy` or any accessories you've declared (full first-run orchestration is tracked in [#2957](https://github.com/wheels-dev/wheels/issues/2957)). Boot those explicitly before (or right after) your first deploy: + + ```bash title="First-run orchestration (run explicitly for now)" + wheels deploy proxy boot + wheels deploy accessory boot all + wheels deploy setup + ``` + + On subsequent deploys you use `wheels deploy` — the proxy and accessories are already running. Expect a few minutes on the first run — Docker pulls the base images for `kamal-proxy`, any accessories, and your app. Output is prefixed with `[host]` so you can see what each server is doing in parallel. @@ -140,10 +156,10 @@ Three quick checks before running anything: Check the container state: ```bash title="Check container state" - wheels deploy app details + wheels deploy app containers ``` - Prints `docker ps` output filtered by your service label, for every host. You should see one running container per host per role. + Prints `docker ps` output filtered by your service label, for every host. You should see one running container per host per role. (`wheels deploy details` gives the wider view — app, proxy, and accessories. `wheels deploy app details` is a different verb: it requires `--release=` and reports a single container's `docker inspect` status.) 8. **Make a change and redeploy.** @@ -155,7 +171,7 @@ Three quick checks before running anything: Same command, no `setup`. The rolling flow kicks in: new image builds, pushes, pulls to every host, then the proxy cuts traffic over host-by-host. Zero downtime — `kamal-proxy` drains in-flight requests to the old container before switching. - The version label is the short git sha by default. Override with `--version=` if you tag releases differently. + The version label is the short git sha by default. Override with `--release=` if you tag releases differently. (`--release` rather than `--version` because the CLI runtime's root parser claims `--version` for itself.) @@ -178,12 +194,12 @@ If something looks wrong, `wheels deploy details` is always the first stop. Every deploy tags its container with the version label (git sha by default). To roll back, you point `wheels deploy rollback` at a previous version: ```bash title="Roll back to a previous version" -wheels deploy rollback --version=abc1234 +wheels deploy rollback abc1234 ``` -Finds containers tagged `abc1234` on every host, starts them, and asks the proxy to switch traffic back. The old containers usually still exist on-host — `wheels deploy` doesn't prune aggressively — so rollback is fast. If you've pruned, the rollback fails at the "no such container" step; re-deploy from that sha instead. +The version is a positional argument, same as Ruby Kamal's `kamal rollback VERSION`. Don't write `--version=abc1234` — the CLI runtime's root parser intercepts `--version` before the deploy module sees it and the command fails ([#2674](https://github.com/wheels-dev/wheels/issues/2674)). -Scoped rollouts and rollbacks are supported via `--hosts=` and `--role=` flags, useful when you want to test a change on one host before fanning out. +Finds containers tagged `abc1234` on every host, starts them, and asks the proxy to switch traffic back. The old containers usually still exist on-host — `wheels deploy` doesn't prune aggressively — so rollback is fast. If you've pruned, the rollback fails at the "no such container" step; re-deploy from that sha instead. ## Troubleshooting diff --git a/web/sites/guides/src/content/docs/v4-0-0/deployment/hooks.mdx b/web/sites/guides/src/content/docs/v4-0-0/deployment/hooks.mdx index 325412d172..302a85961e 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/deployment/hooks.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/deployment/hooks.mdx @@ -27,20 +27,19 @@ You've read [Your First Deploy](/v4-0-0/deployment/first-deploy/). Hooks are use |------|-------|---------| | `pre-deploy` | Before any host work starts. | Yes — non-zero exit aborts the deploy before anything on the servers changes. | | `post-deploy` | After a successful deploy. | Yes — non-zero exit fails the deploy after-the-fact, useful for smoke tests. | -| `post-deploy-failure` | After a deploy that threw an error. | No — this is already a failure path; the exit code is logged but doesn't change what happens. | +| `post-deploy-failure` | After a deploy that threw an error. | Already a failure path — but make the hook itself exit 0. A non-zero exit currently throws and **replaces the original deploy error** in the output, masking the root cause ([#3087](https://github.com/wheels-dev/wheels/issues/3087)). | All three live under `.kamal/hooks/` and must be executable (`chmod +x`). They run on the control machine — the same machine running `wheels deploy` — not on the target hosts. ## Environment variables every hook receives -`wheels deploy` fires every hook with a `KAMAL_*` env block that matches Ruby Kamal's contract exactly. Keeping the prefix `KAMAL_` (not `WHEELS_`) means any hook written for Ruby Kamal works unchanged — you can port hooks between the two tools without editing. +`wheels deploy` fires every hook with a `KAMAL_*` env block that follows Ruby Kamal's naming. Keeping the prefix `KAMAL_` (not `WHEELS_`) means hooks written for Ruby Kamal that read these variables work unchanged — you can port them between the two tools without editing. The block is a subset of Ruby Kamal's full contract: variables like `KAMAL_ROLE` and `KAMAL_SERVICE` are not set. | Variable | When available | Value | |----------|---------------|-------| | `KAMAL_VERSION` | all events | Version being deployed (git short sha by default). | | `KAMAL_HOSTS` | all events | Comma-separated list of hosts in the deploy. | -| `KAMAL_PERFORMER` | all events | Local user running `wheels deploy` (`$USER`). | -| `KAMAL_ROLE` | all events | Role being deployed, or empty when the deploy spans roles. | +| `KAMAL_PERFORMER` | all events | `git config user.name`, falling back to `$USER` when unset. | | `KAMAL_DESTINATION` | all events | Value of `--destination`, or empty. | | `KAMAL_RUNTIME` | `post-deploy`, `post-deploy-failure` | Seconds elapsed since the deploy started. | | `KAMAL_ERROR` | `post-deploy-failure` only | Error message that stopped the deploy. | @@ -83,7 +82,8 @@ The non-zero exit stops `wheels deploy` before it touches production. ```bash title=".kamal/hooks/post-deploy-failure (illustrative — do not type)" #!/usr/bin/env bash -set -euo pipefail +# Deliberately no `set -e`: exit 0 even if the page fails, so the +# original deploy error stays visible (see #3087). curl -sS -X POST https://events.pagerduty.com/v2/enqueue \ -H 'Content-Type: application/json' \ @@ -98,7 +98,7 @@ curl -sS -X POST https://events.pagerduty.com/v2/enqueue \ } } EOF -)" +)" || true ``` ## Output @@ -112,7 +112,7 @@ Hook stdout and stderr are merged and prefixed with `[hook:]` in the deplo A non-zero exit from `pre-deploy` aborts the deploy before any server work. A non-zero exit from `post-deploy` marks the deploy as failed after-the-fact — the containers are already rolled over, so this is useful for integration smoke tests that gate "did the deploy actually work" rather than "can I start the new container." -`post-deploy-failure` never changes the exit code; it runs best-effort on an already-failed path. +`post-deploy-failure` runs on an already-failed path, so the overall exit stays non-zero either way — but a non-zero exit from the hook itself currently throws and **replaces the original deploy error** in the output ([#3087](https://github.com/wheels-dev/wheels/issues/3087)). Until that's fixed, write `post-deploy-failure` hooks to always exit 0 (swallow notification failures with `|| true`) so the real error survives. ## Debugging hooks @@ -141,7 +141,7 @@ KAMAL_PERFORMER=$USER \ --` (e.g. `myapp-web-abc1234`) | | Container labels | `service=`, `role=`, `destination=`, `version=` | -| Docker network | `kamal` | +| Docker network | `kamal` — *pending #2957: the network is not created yet* | | Proxy image | `basecamp/kamal-proxy:v0.8.6` | -| Proxy config dir | `/home//.config/kamal-proxy/` | +| Proxy config dir | `/home//.config/kamal-proxy/` — *pending #2957: currently computed as `/home/` even when deploying as root (Kamal uses `/root`)* | | Lock file path | `/tmp/kamal_deploy_lock_` | -| Audit log | `/tmp/kamal-audit.log` | +| Audit log | `/tmp/kamal-audit.log` — *pending #2957: never written yet* | | Hook directory | `.kamal/hooks/` | | Hook env prefix | `KAMAL_*` (not `WHEELS_*`) | | Secret file | `.kamal/secrets`, `.kamal/secrets.` | -Why keep the `KAMAL_*` prefix? Because every hook anyone has ever written for Ruby Kamal uses it. Renaming would break every user's existing scripts for zero benefit. +Why keep the `KAMAL_*` prefix? Because every hook anyone has ever written for Ruby Kamal uses it. Renaming would break every user's existing scripts for zero benefit. One honesty note: the env block is a **subset** of Ruby Kamal's — `KAMAL_VERSION`, `KAMAL_HOSTS`, `KAMAL_PERFORMER`, `KAMAL_DESTINATION`, `KAMAL_RUNTIME`, and `KAMAL_ERROR` are set; `KAMAL_ROLE`, `KAMAL_SERVICE`, and the rest are not. Hooks reading only the supported subset port unchanged; also make `post-deploy-failure` hooks exit 0 — a non-zero exit currently masks the original deploy error ([#3087](https://github.com/wheels-dev/wheels/issues/3087)). See [Hooks](/v4-0-0/deployment/hooks/) for the full table. Why keep the `.kamal/` directory name? Because you can sit on both tools during evaluation — `kamal deploy` and `wheels deploy` read the same secrets and the same hooks. Switch back and forth freely; nothing on the server changes. @@ -63,10 +63,9 @@ image: ${REGISTRY}/${APP_NAME} `${VAR}` references resolve in this order: -1. CLI `--env` overrides (test/CI shims). -2. `.kamal/secrets` (and the destination overlay `.kamal/secrets.` when `--destination` is set). -3. `System.getenv(VAR)` on the machine running `wheels deploy`. -4. Empty string (Kamal's behavior for unset vars). +1. `.kamal/secrets` (and the destination overlay `.kamal/secrets.` when `--destination` is set). Caveat: the lookup is currently relative to `deploy.yml`'s directory, so it reads `config/.kamal/secrets` rather than the project-root `.kamal/secrets` ([#3084](https://github.com/wheels-dev/wheels/issues/3084)). +2. `System.getenv(VAR)` on the machine running `wheels deploy`. +3. Empty string (Kamal's behavior for unset vars). Only uppercase-and-underscore tokens are expanded — `${APP_NAME}` matches; lowercase `${service}` is left alone (this prevents accidental capture of shell-style placeholders elsewhere in the config). This is the same rule Kamal applies. @@ -78,7 +77,7 @@ If your existing config uses ERB logic — `<%= "blue" if ENV["ROLE"] == "stagin |----------------|-----------------| | `<%= ENV["FOO"] %>` | `${FOO}` | | `<%= ENV.fetch("FOO", "bar") %>` | `${FOO}` — set `FOO=bar` in `.kamal/secrets` for the default | -| `<%= `git rev-parse --short HEAD`.chomp %>` | pass `--version=$(git rev-parse --short HEAD)` on the CLI, or set `VERSION` in `.kamal/secrets` and use `${VERSION}` | +| `<%= `git rev-parse --short HEAD`.chomp %>` | pass `--release=$(git rev-parse --short HEAD)` on the CLI, or set `VERSION` in `.kamal/secrets` and use `${VERSION}` | | `<%= ENV["STAGE"] == "prod" ? 1 : 0 %>` | set the resolved value in `.kamal/secrets.production` or `.kamal/secrets.staging` — destination overlays replace in-file logic | ## What changes in the CLI @@ -87,20 +86,20 @@ The verb surface mirrors Kamal's. Most invocations are identical save the leadin | Ruby Kamal | `wheels deploy` | |------------|-----------------| -| `kamal setup` | `wheels deploy setup` | +| `kamal setup` | `wheels deploy setup` — *currently an alias for `wheels deploy`; unlike Kamal it doesn't boot the proxy or accessories yet ([#2957](https://github.com/wheels-dev/wheels/issues/2957)) — run `wheels deploy proxy boot` and `wheels deploy accessory boot all` yourself* | | `kamal deploy` | `wheels deploy` | | `kamal redeploy` | `wheels deploy redeploy` | -| `kamal rollback VERSION` | `wheels deploy rollback --version=VERSION` | +| `kamal rollback VERSION` | `wheels deploy rollback VERSION` | | `kamal config` | `wheels deploy config` | | `kamal app logs --follow` | `wheels deploy app logs --follow` | | `kamal proxy boot` | `wheels deploy proxy boot` | | `kamal accessory boot db` | `wheels deploy accessory boot db` | -| `kamal secrets fetch --from op://...` | `wheels deploy secrets fetch --adapter=op --from=op://...` | +| `kamal secrets fetch --from op://...` | `wheels deploy fetch-secrets --adapter=op --from=op://...` | | `kamal version` | `wheels deploy version` | Two CLI-level differences worth flagging: -- **Flag style.** `wheels deploy` uses `--flag=value` everywhere. Positional args for a few verbs (`accessory boot `, `rollback ` in Kamal) become named flags (`accessory boot --name=`, `rollback --version=`) for consistency across the CFML arg parser. +- **Some nested verbs are flat.** The CLI runtime claims a few top-level subcommand names (`server`, `secrets`) and the root parser claims `--version`, so those Kamal forms get intercepted before the deploy module sees them. The working spellings are flat: `wheels deploy bootstrap` (not `server bootstrap`, [#2677](https://github.com/wheels-dev/wheels/issues/2677)), `wheels deploy fetch-secrets` / `extract-secrets` / `print-secrets` (not `secrets fetch|extract|print`, [#2697](https://github.com/wheels-dev/wheels/issues/2697)), and positional `rollback VERSION` (never `--version=`, [#2674](https://github.com/wheels-dev/wheels/issues/2674)). Kamal's positional arguments (`accessory boot db`, `rollback VERSION`) are required, exactly as in Kamal — named-flag spellings like `accessory boot --name=db` do not exist. - **`--dry-run` is everywhere.** Every `wheels deploy` verb accepts `--dry-run` and prints the commands it would have run, prefixed by host. This is wider coverage than Kamal's `KAMAL_DEBUG=1` logs. ## The coexistence guarantee @@ -150,6 +149,6 @@ These Ruby Kamal features have no `wheels deploy` equivalent and won't get one: diff --git a/web/sites/guides/src/content/docs/v4-0-0/deployment/observability-and-logging.mdx b/web/sites/guides/src/content/docs/v4-0-0/deployment/observability-and-logging.mdx index 3be1dca8ac..9302dd0e08 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/deployment/observability-and-logging.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/deployment/observability-and-logging.mdx @@ -47,10 +47,13 @@ If you terminate TLS in a reverse proxy that already stamps `X-Request-Id` (ngin component implements="wheels.middleware.MiddlewareInterface" output="false" { public string function handle(required struct request, required any next) { - local.inbound = request.cgi.http_x_request_id ?: ""; + // Read the real `cgi` scope. The middleware request context only + // carries {params, route, pathInfo, method} — it has no `cgi` key, + // so `request.cgi.http_x_request_id` would always be empty. + local.inbound = cgi.http_x_request_id ?: ""; local.id = Len(local.inbound) ? local.inbound : CreateUUID(); - request.wheels.requestId = local.id; + $writeRequestId(local.id); local.response = arguments.next(arguments.request); try { @@ -59,6 +62,17 @@ component implements="wheels.middleware.MiddlewareInterface" output="false" { return local.response; } + + // Write via a helper with no `request` parameter in scope: inside + // handle(), the `required struct request` parameter shadows the request + // scope on Adobe CF, so a bare `request.wheels.requestId = ...` there + // would write to the passed struct instead of the scope. + private void function $writeRequestId(required string requestId) { + if (!StructKeyExists(request, "wheels")) { + request.wheels = {}; + } + request.wheels.requestId = arguments.requestId; + } } ``` @@ -95,7 +109,9 @@ component output="false" { "level": arguments.level, "message": arguments.message, "requestId": request.wheels.requestId ?: "", - "environment": get("environment") + // Bare get() isn't available in a plain component — it's a + // framework mixin. Read the application scope directly. + "environment": application.wheels.environment ?: "" }; StructAppend(local.payload, arguments.context, true); @@ -157,6 +173,12 @@ Wheels does not provide a built-in health check route. Add one — every load ba ```cfm {test:compile} title="app/controllers/Health.cfc" component extends="Controller" { + function config() { + // Without provides(), renderWith() falls back to the HTML view + // (which doesn't exist) and the endpoint 500s. + provides("html,json"); + } + function index() { local.checks = { "database": $checkDatabase(), @@ -164,15 +186,18 @@ component extends="Controller" { }; local.healthy = local.checks.database.ok; - local.status = local.healthy ? 200 : 503; + local.statusCode = local.healthy ? 200 : 503; + // The argument is `status` — renderWith() has no `statusCode` + // argument, and an unknown argument would be swallowed, returning + // HTTP 200 to the load balancer even on the degraded path. renderWith( data = { "status": local.healthy ? "ok" : "degraded", "checks": local.checks, "requestId": request.wheels.requestId ?: "" }, - statusCode = local.status + status = local.statusCode ); } @@ -286,7 +311,7 @@ Every event captured during that request then carries the user context. ### What it doesn't cover -- **Background jobs** — `Job.cfc` writes failures to the `wheels_jobs` log file (`vendor/wheels/Job.cfc` lines 345 and 368) but does not push them to Sentry. If you want Sentry visibility on job failures, wrap `perform()` in a `try/catch` inside each job and call `application.sentry.captureException()` on the rescue path. +- **Background jobs** — `Job.cfc` writes failures to the `wheels_jobs` log file (see the retry and permanent-failure `writeLog` calls in `vendor/wheels/Job.cfc`) but does not push them to Sentry. If you want Sentry visibility on job failures, wrap `perform()` in a `try/catch` inside each job and call `application.sentry.captureException()` on the rescue path. - **Middleware** — exceptions thrown inside middleware bypass controller mixins. Use `application.sentry.captureException()` directly if you need coverage there. ## Metrics and APM @@ -305,7 +330,7 @@ A small set of signals covers almost every production incident a Wheels app will - **5xx rate** — from the reverse proxy access log or the APM agent. Alert when the ratio of 5xx responses crosses a threshold (1% over five minutes is a reasonable starting point). - **Response time p95** — the tail, not the average. A p95 creeping from 200 ms to 1.5 s signals a problem before the average moves. -- **Job queue depth** — `wheels jobs status` reports pending, processing, and failed counts per queue. Alert on pending counts that don't drain or failed counts that grow. +- **Job queue depth** — `(new wheels.Job()).queueStats()` reports pending, processing, and failed counts per queue; expose it from a (protected) controller action or a scheduled task that pushes the numbers to your metrics pipeline. Alert on pending counts that don't drain or failed counts that grow. (There is no `wheels jobs status` CLI command — a worker CLI is tracked in [#3090](https://github.com/wheels-dev/wheels/issues/3090).) - **Database connection pool saturation** — engine-specific metric. Lucee exposes datasource stats; Adobe does too. A pool pinned at its ceiling means requests are queuing to talk to the database. - **Health check failing** — your load balancer's view of app reachability. One pod occasionally failing is noise; all pods failing is an outage. - **Sentry error rate** — alert when the rate of captured exceptions for a release exceeds a baseline, or when a new error fingerprint appears post-deploy. diff --git a/web/sites/guides/src/content/docs/v4-0-0/deployment/secrets.mdx b/web/sites/guides/src/content/docs/v4-0-0/deployment/secrets.mdx index ea337557b3..508f6eb49c 100644 --- a/web/sites/guides/src/content/docs/v4-0-0/deployment/secrets.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0/deployment/secrets.mdx @@ -8,14 +8,18 @@ sidebar: import { Aside, CardGrid, LinkCard } from '@astrojs/starlight/components'; -`.kamal/secrets` is the plain-text glue between your secret store and `deploy.yml`. It's read on the control machine at deploy time, values are resolved, and the resulting `KEY=VALUE` map is passed into containers as environment variables. The file itself is never under git — the whole point is that it's the bridge between "secrets that live in a vault" and "env vars that live in a container." +`.kamal/secrets` is the plain-text glue between your secret store and `deploy.yml`. It's read on the control machine at deploy time and values are resolved there. The file itself is never under git — the whole point is that it's the bridge between "secrets that live in a vault" and "values the deploy flow needs at runtime." + + **You'll learn:** - The format of `.kamal/secrets` - How `$(...)` subshell substitution pulls values from any CLI tool - The five built-in adapters for common vaults -- How secrets flow from `.kamal/secrets` through `deploy.yml` into the container +- How secrets flow from `.kamal/secrets` through `deploy.yml` at deploy time — and which leg of that flow is still pending