Content: the Kamal 2 guide was documenting Kamal 1 (page-1 rankings, 0% CTR) - #506
Merged
Merged
Conversation
The published post ranked page-1 for "kamal deploy minimum ram" (9.5),
its variant (9.3) and "kamal 2.0" (7.5) - and took 0 clicks on 32
impressions over 28 days. It earned the traffic while being wrong.
Verified against the real gem (kamal-2.12.0, fetched and unpacked), not
from memory. What the old post taught vs what Kamal 2 does:
traefik: block in the flagship deploy.yml
-> Traefik is REMOVED in Kamal 2. cli/proxy.rb:151 cleanup_traefik,
main.rb:204 "replace Traefik with kamal-proxy",
configuration.rb:383 rejects pre/post-traefik-reboot hooks.
Correct config is proxy: ssl/host.
top-level healthcheck: with port:/max_attempts:
-> lives under proxy: with interval/path/timeout
(configuration/docs/proxy.yml:117)
.env.erb + .env secrets model
-> kamal init creates .kamal/secrets (cli/main.rb:159-163).
The old post never mentioned .kamal/secrets once, across 5 places
that taught .env. The whole secrets workflow was a major version
behind.
bin/kamal "created by init"
-> only with --bundle, which defaults false (cli/main.rb:147)
strategy: blue_green / max_surge / max_unavailable / deployment:
-> ZERO matches anywhere in the gem. Invented config. Kamal's real
rolling deploy is boot: limit/wait.
"Ruby 2.7+ required" (EOL 2023), "Ubuntu 20.04+" (EOL Apr 2025)
A reader pasting the old flagship deploy.yml got a deployment that does
not work. That is the same defect class as the P0 fixed in #498.
Also fixed, per review:
- duplicate H1: single.html:47 already renders <h1>{{ .Title }}</h1>,
and the body repeated the title verbatim - two identical H1s
- repetition: config.force_ssl taught 3x (221/589/661), production.rb
edited across 5 blocks, 3 overlapping performance sections, 3 closing
sections
- unsourced cost table ($155/mo, "54% savings", "75% savings") - deleted
rather than re-sourced; it was a digression in a deploy tutorial
- dev_to_id: 1 placeholder (same field #498 stripped from the Falcon post)
- flat file -> page bundle so cover.png can be a bundle resource
URL is preserved. The old file had no slug:, so Hugo derived the permalink
from the title; an explicit slug: now pins the same URL while the title
drops the stale "in 2025" that was showing in the SERP.
Scope: this post owns first-deploy intent (setup, install, rollback, RAM).
kamal-2-multi-server-deployment-complete-guide (2026-08-07) already owns
multi-server correctly, so roles, boot limits and multi-host TLS hand off
to it instead of being re-taught here. Found via qmd, which surfaced the
content-plan row showing both target "kamal 2.0".
Still to come on this branch: cover.png, pre-rendered mermaid SVG, gates.
Cover follows the 6-slot house layout. Chips carry the post's three
findings rather than decoration: kamal-proxy not Traefik, .kamal/secrets,
build runs on your laptop. Status reads KAMAL 2.12.
First render wrapped hero line 3 ("Rails on your own / servers") and
stranded one word on its own line, turning a 3-line template into 4.
Shortened to "Your own servers" and re-rendered. Scored after looking at
it: look OK, readable without zoom, earns the scroll (the Traefik chip
tells you something), chips helpful not decorative.
Mermaid pre-rendered to SVG at authoring time per bin/render-mermaid, so
the page ships no mermaid.js. Vertical 4-node chain keeps viewBox at
272x563 - 12px min font renders ~17.2px at 390px wide, clear of the 9px
floor, and 563px tall is under one mobile viewport so it is not a wall.
bin/hugo-build: 8/8 validators, 1174 pages. URL preserved at
/blog/kamal-2.0-complete-rails-deployment-guide-deploy-without-heroku-in-2025/.
Post now emits one H1 (the theme's), matching the clean sibling post - the
site-wide "Welcome to JetThoughts" hero H1 is pre-existing on every page
and is not touched here.
…ning kamal
A 4-eyes reviewer RAN `kamal config` against the post's own deploy.yml
instead of reading the gem, and falsified the article's opening thesis.
I re-ran all three myself before fixing.
1. The hook was inverted. I wrote that pasting a `traefik:` block into a
Kamal 2 config produces no warning. It produces:
ERROR (Kamal::ConfigurationError): unknown key: traefik
Kamal validates top-level keys and refuses. My error was reading
Validator::Configuration#allow_extensions? => true and concluding
unknown keys pass - that flag is for YAML extension keys, not
arbitrary config keys. The same read-source-assert-behavior mistake
this post exists to correct.
2. Same for a top-level `healthcheck:` block - also rejected. The old
wording contradicted our own multi-server post, which correctly says
Kamal rejects unrecognized keys at validation time. That post was
right and this one was wrong.
Both now show the real error text, and the section turns the
validator into the porting tool it actually is.
3. `kamal config` does not print secrets or env. configuration.rb#to_h
emits roles/hosts/image/builder/accessories only; the gem's own desc
string ("including secrets!") is stale and I inherited it. Verified:
mistyping a secret name leaves output byte-identical and exit 0. The
post told a stalled reader to run a command that cannot show what it
claimed - worse than no advice at all.
Also corrected against the gem:
- setup is server:bootstrap + deploy(boot_accessories: true); there is
no env-push step (no cli/env.rb in 2.12)
- upgrade confirms ONCE, then runs both sub-upgrades with confirmed: true
- traefik reboot hooks should be RENAMED to (pre|post)-proxy-reboot, per
the error's own text - "delete those files" threw away working hooks
- rollback gates on a container, not an image, and prune keeps 5 by
default (configuration.rb:216) - that retention is the real limit on
how far back you can roll
- the rollback example was a literal bracketed placeholder that errors
when pasted; now a real SHA
Voice: dropped a first-person-singular "the question I get most" under a
Team byline, an "almost everyone" generalization, an unsourced
superlative, and a rule-of-three in twitter_description.
bin/hugo-build green. URL unchanged.
.stitch/designs/ is gitignored, but the cover HTMLs that back published posts are force-added by convention - falcon-production-tuning-cover.html (this one's parent), rails-8-authentication, solid-queue-advanced and two others are all tracked. Without the source, regenerating or tweaking this cover means rebuilding the 6-slot layout from scratch.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… port Paul asked for a prompt a reader can hand to their AI agent instead of doing the migration by hand. The prompt's spine is that kamal config is a real validator: it raises ConfigurationError on every unrecognised top-level key, so the agent gets a loop it can close without human judgement. It is told not to declare the port finished until that command exits 0. Two guards matter more than the key mapping. First, most Kamal material in training data predates Kamal 2, so the prompt explicitly forbids relying on recall and points at kamal docs <section> - the reference that ships inside the installed gem. Second, it forbids inventing keys, which is the exact failure the post documents: the guide this replaces carried strategy: blue_green and max_surge, neither of which Kamal has ever had. An agent trained on pages like that will reach for them. The prompt also refuses to touch credentials (names only, keep .env until the human confirms), renames traefik reboot hooks rather than deleting them, and stops before deploy/setup/upgrade so a person reads the diff before anything restarts real infrastructure. bin/hugo-build: 8/8 validators.
I wrote this post's citation list as a bare 'Further reading:' paragraph, copying the form its RubyLLM neighbours used. That form is drift - the repo's convention is '## Sources', which 4 posts already used and 16 more are being converted to on chore/further-reading-typography. Fixing it here so this post doesn't merge as the 17th instance of a pattern being removed in parallel. bin/hugo-build: 8/8 validators.
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
The repo was split: 9 posts used '## Further reading', 4 used a bare 'Further reading:' paragraph. The 4 are the whole RubyLLM cluster, and the Kamal guide on #506 copied the same plain-text form from its neighbours - so the drift was still spreading. As plain text it is a paragraph ending in a colon: no table-of-contents entry, no landmark for a screen reader, and no visual weight separating it from the body above it. The heading form is what the other 9 posts already do. Content unchanged - only the marker line.
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
…ts (#510) * chore(content): make 'Further reading' a real heading everywhere The repo was split: 9 posts used '## Further reading', 4 used a bare 'Further reading:' paragraph. The 4 are the whole RubyLLM cluster, and the Kamal guide on #506 copied the same plain-text form from its neighbours - so the drift was still spreading. As plain text it is a paragraph ending in a colon: no table-of-contents entry, no landmark for a screen reader, and no visual weight separating it from the body above it. The heading form is what the other 9 posts already do. Content unchanged - only the marker line. * chore(content): rename 'Further reading' to the existing 'Sources' convention Paul flagged that 'Further reading' and the theme's auto-generated 'Read next' both read as 'more things to read', with nothing signalling that one is external citations and the other is internal JT posts. Checked before renaming: every one of these lists is 100% external. The internal links that appear near them live on a separate 'Related:' prose line (2 posts), not inside the list - an earlier count of mine said otherwise because it read to end-of-file rather than to the end of the section. 'Sources' is not a new name. 4 posts on master already used '## Sources'; 16 had drifted to 'Further reading' in three different forms: 9 ## Further reading 4 Further reading: (bare paragraph) 3 **Further reading:** (bold - missed by my first survey, which only matched the bare and heading forms) All 16 now match the 4. Twenty posts, one convention. What this does NOT touch: the 'Related:' prose line and the theme's 'Read next' (related-posts.html:11) are both internal links and still overlap in meaning. That is pre-existing and a separate editorial call - 15 posts already opt out of 'Read next' via related_posts: false. bin/hugo-build: 8/8 validators, 1174 pages. Marker lines only.
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
Sync for 2026-08-20's merged work (#499, #501, #506, #509, #510). All three land in workflows/blog-pipeline.md because they are gates, not background - per the bundle's own rule that a lesson mattering six weeks later belongs in a concept rather than the log. 1. Technical claims must be executed, not read. Every wrong technical claim shipped today came from reading source and inferring behaviour. The Kamal guide asserted a stale traefik: key sits in deploy.yml doing nothing; kamal config answers 'unknown key: traefik'. The bad inference came from Validator::Configuration#allow_extensions? => true, which governs YAML extension keys and not arbitrary config keys. 2. Frontmatter is published copy. #509 shipped live with a body softened to 'whether retired or quietly substituted' while twitter_description still asserted 'a retired model took out five features at once'. 3. Citation lists use ## Sources. 4 posts already used it; 16 had drifted across three forms, and the bold variant survived the first survey because my grep matched only the other two. Concept timestamp + verified stamp added honestly (claude/opus-5, today). workflows/index.md entry widened to name the three gates so a cold session finds them without opening the file. Validated: uv run okf_validate.py .okf --strict -> conformant, 0 errors. Warning count unchanged at 64 (measured against a clean tree first, so the number is a baseline rather than a claim).
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
docs(okf): three publishing gates learned from claims we shipped wrong Sync for 2026-08-20's merged work (#499, #501, #506, #509, #510). All three land in workflows/blog-pipeline.md because they are gates, not background - per the bundle's own rule that a lesson mattering six weeks later belongs in a concept rather than the log. 1. Technical claims must be executed, not read. Every wrong technical claim shipped today came from reading source and inferring behaviour. The Kamal guide asserted a stale traefik: key sits in deploy.yml doing nothing; kamal config answers 'unknown key: traefik'. The bad inference came from Validator::Configuration#allow_extensions? => true, which governs YAML extension keys and not arbitrary config keys. 2. Frontmatter is published copy. #509 shipped live with a body softened to 'whether retired or quietly substituted' while twitter_description still asserted 'a retired model took out five features at once'. 3. Citation lists use ## Sources. 4 posts already used it; 16 had drifted across three forms, and the bold variant survived the first survey because my grep matched only the other two. Concept timestamp + verified stamp added honestly (claude/opus-5, today). workflows/index.md entry widened to name the three gates so a cold session finds them without opening the file. Validated: uv run okf_validate.py .okf --strict -> conformant, 0 errors. Warning count unchanged at 64 (measured against a clean tree first, so the number is a baseline rather than a claim).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Paul flagged the Kamal guide as "not top world class" - duplicated title, irrelevant paragraphs, repetition, and suspected wrong versions. All four were real, and the version problem was worse than stale numbers.
The post was a Kamal 1 guide wearing a Kamal 2 title. Every claim below was checked against the real gem (
kamal-2.12.0, fetched and unpacked), not from memory.traefik:block in the flagshipdeploy.ymlcleanup_traefik, replaced by kamal-proxy, and traefik reboot hooks are rejectedcli/proxy.rb:151,main.rb:204,configuration.rb:383healthcheck:w/port:,max_attempts:proxy:withinterval/path/timeoutconfiguration/docs/proxy.yml:117.env.erb+.envsecrets, taught in 5 places.kamal/secrets- never mentioned once in the old postcli/main.rb:159-163bin/kamalcreated byinit--bundle, default falsecli/main.rb:147strategy: blue_green,max_surge,max_unavailableA reader pasting the old flagship
deploy.ymlgot a deployment that does not work. Same defect class as the P0 in #498.What the live data said
32 impressions, 0 clicks, 0.0% CTR over 28 days - while ranking 9.5 for "kamal deploy minimum ram", 9.3 for its variant, and 7.5 for "kamal 2.0". It earned page-one placement and got passed over, with
Deploy Without Heroku in 2025in the SERP title in August 2026.Also fixed
single.html:47already renders<h1>{{ .Title }}</h1>; the body repeated it verbatim. The post now emits one H1, matching the clean sibling post. (The site-wide "Welcome to JetThoughts" hero H1 exists on every page - pre-existing, not touched here, worth its own ticket.)config.force_ssltaught 3x (221/589/661),production.rbedited across 5 separate blocks, 3 overlapping performance sections, 3 closing sections.dev_to_id: 1placeholder (the same field P0: recover #2 organic page from a 404, fix unbootable published configs + broken sample code #498 stripped from the Falcon post).cover.pngcan be a bundle resource.Scope, and why this isn't a duplicate
qmdsurfaced the content-plan row showingkamal-2-multi-server-deployment-complete-guide(published 2026-08-07) targets the same "kamal 2.0" keyword. That post is correct Kamal 2 and owns multi-server.This post takes first-deploy intent - which is what it actually ranks for (
setup,install,rollback, RAM) - and hands roles, boot limits and multi-host TLS off to the sibling rather than re-teaching them.The review caught me fabricating, twice
A
core-reviewerpass rankamal configinstead of reading the gem, and falsified my opening thesis. I re-ran all three myself before fixing:traefik:block produces no warning. It producesERROR (Kamal::ConfigurationError): unknown key: traefik. Kamal validates top-level keys and refuses. My error: I readValidator::Configuration#allow_extensions? => trueand concluded unknown keys pass - that flag is for YAML extension keys, not arbitrary config keys.healthcheck:. My wording contradicted our own multi-server post, which correctly says Kamal rejects unrecognized keys at validation. That post was right; this one was wrong.kamal configdoes not print secrets or env -to_hemits roles/hosts/image/builder/accessories only. The gem's own help string ("including secrets!") is stale and I inherited it. Verified: mistyping a secret name leaves output byte-identical, exit 0. The post sent a stalled reader to a command that cannot show what it claimed.Also corrected:
setuphas no env-push step;upgradeconfirms once, not per step; traefik hooks should be renamed to(pre|post)-proxy-rebootper the error's own text, not deleted;rollbackgates on a container andprunekeeps 5 by default, which is the real limit on how far back you can roll; the rollback example was a literal bracketed placeholder that errors when pasted.The corrected framing is a better post: Kamal 2 fails loudly, so the useful content is the key mapping.
Gates
bin/hugo-build: 8/8 validators, 1174 pages/blog/kamal-2.0-complete-rails-deployment-guide-deploy-without-heroku-in-2025/- the old file had noslug:, so Hugo derived the permalink from the title; an explicitslug:now pins the same URL while the title drops the stale "in 2025"qtest/test/dtestcorrectly out of scope per CLAUDE.mdOpen item for you
The visible date still reads JAN 15, 2025. Keeping it preserves the page's age signal, but a reader in August 2026 sees a 2025 date on a post about Kamal 2.12. I did not change it unilaterally - bumping
date:would re-sort the blog index and rewrite the publication history. Say the word if you want it bumped orlastmodsurfaced in the template.🤖 Generated with Claude Code