<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-4670815028761617050</id><updated>2026-09-22T15:14:41.126+02:00</updated><category term="AI"/><category term="Alerting"/><category term="Claude"/><category term="Documentation"/><category term="Engineering"/><category term="GitHub Copilot"/><category term="Grafana"/><category term="Hetzner"/><category term="Kubernetes"/><category term="Monitoring"/><category term="OpenAI Codex"/><category term="Prometheus"/><category term="Series"/><category term="Telegram"/><title type='text'>MisLinux</title><subtitle type='html'>Practical notes on Kubernetes, Hetzner, Linux, networking, and cost-aware infrastructure.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://mislinux.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>Unknown</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>30</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-7997000632674655489</id><published>2026-06-16T12:14:41.750+02:00</published><updated>2026-06-16T12:37:37.693+02:00</updated><title type='text'>How I Manage AI Agents Like a New Hire (Detailed Briefs, Reviewed Output, CIR Notes)</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Manage AI Agents Like a New Hire (Detailed Briefs, Reviewed Output, CIR Notes) --&gt;
&lt;!-- Suggested labels: AI, Automation, Operations, Documentation, Engineering, Management --&gt;
&lt;!-- Suggested meta description: I do not treat AI as a button that magically finishes chores. I delegate work like I would to a capable new hire: precise briefs, reviewed deliverables, scripts that survive the session, and AGENTS.md CIR notes that capture why. --&gt;
&lt;!-- Cover image source file: html/images/post-32-cover.png --&gt;

&lt;div style=&quot;margin:0 0 1.4em 0;padding:0.95em 1.05em;border:1px solid #d0d7de;border-radius:10px;background:#f6f8fa;&quot;&gt;&lt;p style=&quot;margin:0;font-size:1.02em;line-height:1.45;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;margin:0.35em 0 0 0;color:#57606a;font-size:0.95em;line-height:1.45;&quot;&gt;MisLinux&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It describes how I actually *manage* AI coding agents day to day — not the tools themselves, but the operating model that keeps them useful in production work.&lt;/p&gt;

&lt;p&gt;Most teams experiment with AI in one of two broken modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Magic button&lt;/strong&gt; — &amp;quot;fix it&amp;quot; with no context, hope for the best.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot as autocomplete&lt;/strong&gt; — fast typing, no durable output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model that works for business and operations automation is a third one:&lt;/p&gt;

&lt;p&gt;&amp;gt; Treat the agent like a &lt;strong&gt;new hire who is fast, literal, and forgetful&lt;/strong&gt; — brilliant at execution when the brief is clear, dangerous when the brief is vague, and unable to remember yesterday unless you wrote it down.&lt;/p&gt;

&lt;h2&gt;The contract I use with every agent session&lt;/h2&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;New-hire parallel&lt;/th&gt;&lt;th&gt;What I ask the AI to do&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Onboarding packet&lt;/td&gt;&lt;td&gt;Point it at &lt;code&gt;AGENTS.md&lt;/code&gt;, relevant &lt;code&gt;SKILL.md&lt;/code&gt;, and proof artifacts from the last run&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Written task brief&lt;/td&gt;&lt;td&gt;State goal, constraints, what must &lt;strong&gt;not&lt;/strong&gt; change, and how success is verified&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Deliverable, not chat&lt;/td&gt;&lt;td&gt;Require a script/program/test/CIR entry — not &amp;quot;here is what you could run&amp;quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Code review&lt;/td&gt;&lt;td&gt;Read the diff myself; rerun dry-run/smoke tests before any production path&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Manager notes&lt;/td&gt;&lt;td&gt;Append Context / Intent / Rationale when behavior or tooling changes&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If the session ends with only prose, I failed the brief.&lt;/p&gt;

&lt;h2&gt;Step 1: Route manual work through the agent&lt;/h2&gt;

&lt;p&gt;When I catch myself doing something twice — spreadsheet reconciliation, onboarding checks, config diff, report export — I stop and rephrase:&lt;/p&gt;

&lt;p&gt;&amp;gt; &amp;quot;Do this for me, but implement it as a repeatable script with tests and a CIR entry. Dry-run first.&amp;quot;&lt;/p&gt;

&lt;p&gt;That single sentence forces three outputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Automation&lt;/strong&gt; — PHP/Python/ shell the agent can rerun without the conversation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guardrails&lt;/strong&gt; — PHPUnit or smoke tests on the invariant logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt; — &lt;code&gt;AGENTS.md&lt;/code&gt; bullet explaining *why* the approach exists&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent is the implementer. I remain the approver.&lt;/p&gt;

&lt;h2&gt;Step 2: Write briefs the way you would for a junior engineer&lt;/h2&gt;

&lt;p&gt;Vague briefs produce confident wrong code. I include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope boundary&lt;/strong&gt; — read-only vs production mutation; which namespace/network/sheet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inputs&lt;/strong&gt; — file paths, env vars, credentials *location* (never the secret itself)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expected output&lt;/strong&gt; — JSON shape, exit codes, artifact directory layout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification&lt;/strong&gt; — exact command I will run to prove it worked&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discarded options&lt;/strong&gt; — &amp;quot;do not patch production CronJob inline; fix the generator&amp;quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example weak brief:&lt;/p&gt;

&lt;p&gt;&amp;gt; Clean up the onboarding mess.&lt;/p&gt;

&lt;p&gt;Example strong brief:&lt;/p&gt;

&lt;p&gt;&amp;gt; Add &lt;code&gt;onboard.php --plan&lt;/code&gt; read-only mode for YAML manifests. Must label EXTERNAL steps, emit JSON for monitoring, PHPUnit for the diff engine, CIR in &lt;code&gt;operations/adsystem/AGENTS.md&lt;/code&gt;, and save proof under &lt;code&gt;history/&amp;lt;timestamp&amp;gt;/&lt;/code&gt;. No live DB writes in this slice.&lt;/p&gt;

&lt;p&gt;The second brief is longer because &lt;strong&gt; ambiguity is expensive&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;Step 3: Review output like a manager, not a spectator&lt;/h2&gt;

&lt;p&gt;I assume the first pass is wrong somewhere. My review checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Did it touch only the files the brief allowed?&lt;/li&gt;
&lt;li&gt;[ ] Does dry-run default to safe?&lt;/li&gt;
&lt;li&gt;[ ] Are tests asserting behavior, not implementation trivia?&lt;/li&gt;
&lt;li&gt;[ ] Does failure output say expected vs actual?&lt;/li&gt;
&lt;li&gt;[ ] Is there a CIR entry with &lt;strong&gt;Rationale&lt;/strong&gt; (not just Context)?&lt;/li&gt;
&lt;li&gt;[ ] Would another operator know how to rerun this in six months?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something is off, I &lt;strong&gt;nudge&lt;/strong&gt;, not restart:&lt;/p&gt;

&lt;p&gt;&amp;gt; &amp;quot;Keep the planner, but EXTERNAL must not count as WRONG. Add a test. Update CIR with why.&amp;quot;&lt;/p&gt;

&lt;p&gt;That preserves context already loaded in the session.&lt;/p&gt;

&lt;h2&gt;Step 4: Demand CIR annotations for every non-obvious choice&lt;/h2&gt;

&lt;p&gt;Models forget. Repositories should not.&lt;/p&gt;

&lt;p&gt;Good CIR captures &lt;strong&gt;why&lt;/strong&gt;, not what:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;- Context: onboarding checks were duplicated in chat and cron.
  Intent: one manifest drives plan, apply, and readiness monitoring.
  Rationale: without a single contract, AI regenerated slightly different
  checks each session; manifest + `--plan` makes drift visible before apply.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I explicitly ask:&lt;/p&gt;

&lt;p&gt;&amp;gt; &amp;quot;Add a CIR entry to the nearest AGENTS.md explaining what you tried, what failed, and what would re-break if reverted.&amp;quot;&lt;/p&gt;

&lt;p&gt;Without that, the next agent (or me in three weeks) &amp;quot;cleans up&amp;quot; the guardrail that prevented an outage.&lt;/p&gt;

&lt;h2&gt;Step 5: Reduce manual touchpoints every iteration&lt;/h2&gt;

&lt;p&gt;Each manual step is randomness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong network selected&lt;/li&gt;
&lt;li&gt;Stale token&lt;/li&gt;
&lt;li&gt;Spreadsheet column renamed overnight&lt;/li&gt;
&lt;li&gt;&amp;quot;I thought we already ran that&amp;quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My metric: &lt;strong&gt;count human decisions per run&lt;/strong&gt;. If a process still needs ten, the next session removes two — with tests.&lt;/p&gt;

&lt;p&gt;Automation does not mean zero humans. It means humans approve &lt;strong&gt;gates&lt;/strong&gt;, not re-type &lt;strong&gt;data&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;What this looks like in practice&lt;/h2&gt;

&lt;p&gt;Recent classes of work where the new-hire model helped:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Chore&lt;/th&gt;&lt;th&gt;Agent built&lt;/th&gt;&lt;th&gt;I approved&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Publisher onboarding diff&lt;/td&gt;&lt;td&gt;YAML manifest + &lt;code&gt;--plan&lt;/code&gt; library&lt;/td&gt;&lt;td&gt;Read-only proof JSON before any apply&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Control panel ad-request filter&lt;/td&gt;&lt;td&gt;Second-pass PHP + PHPUnit&lt;/td&gt;&lt;td&gt;One-off kubectl job logs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Blogger publish recovery&lt;/td&gt;&lt;td&gt;CDP publish helper fixes + crawl gate&lt;/td&gt;&lt;td&gt;Public HTML verification&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Finance month fill&lt;/td&gt;&lt;td&gt;Mapping libraries + failure email contract&lt;/td&gt;&lt;td&gt;Dry-run Job in cluster&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;In every case, the valuable artifact was &lt;strong&gt;not&lt;/strong&gt; the chat. It was the script + test + CIR triad.&lt;/p&gt;

&lt;h2&gt;Anti-patterns I stop early&lt;/h2&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Anti-pattern&lt;/th&gt;&lt;th&gt;Why it fails&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&amp;quot;Just do it live&amp;quot;&lt;/td&gt;&lt;td&gt;No replay, no audit trail&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Accepting code without tests&lt;/td&gt;&lt;td&gt;Regresses silently on next model pass&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Letting AI edit production YAML by hand&lt;/td&gt;&lt;td&gt;Drifts from generator source of truth&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Skipping CIR &amp;quot;because it is obvious&amp;quot;&lt;/td&gt;&lt;td&gt;Obvious fades in two weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Treating refusal as failure&lt;/td&gt;&lt;td&gt;Good agent should block unsafe mutations&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h2&gt;Sibling posts on this blog&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/03/how-i-use-cir-notes-to-make-ai-coding.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Use CIR Notes to Make AI Coding Agents More Useful&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/06/business-automation-through-ai-how-i.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;Business Automation Through AI: Scripts, Tests, and AGENTS.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/06/how-i-fixed-cursor-getting-slower-every.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Fixed Cursor Getting Slower Every Message&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they describe one strategy: &lt;strong&gt;delegate execution to the agent, keep judgment and memory in the repo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independence note:&lt;/strong&gt; AI tools mentioned (Copilot, Claude, Codex, Cursor) reflect my stack. No vendor sponsorship implied.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7997000632674655489'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7997000632674655489'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/how-i-manage-ai-agents-like-new-hire.html' title='How I Manage AI Agents Like a New Hire (Detailed Briefs, Reviewed Output, CIR Notes)'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-4816176447333411271</id><published>2026-06-16T12:13:02.642+02:00</published><updated>2026-06-16T12:13:02.642+02:00</updated><title type='text'>How I Fixed Cursor Getting Slower Every Message (Desktop + CLI, Any OS)</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Fixed Cursor Getting Slower Every Message (Desktop + CLI, Any OS) --&gt;
&lt;!-- Suggested labels: Cursor, AI, Developer Tools, Productivity, Linux, Windows, macOS --&gt;
&lt;!-- Suggested meta description: Chat lag from indexer churn — I fixed it with a root `.cursorignore` that keeps history, caches, and generated artifacts out of Cursor IDE and CLI indexing. --&gt;
&lt;!-- Cover image source file: html/images/post-29-cover.png --&gt;

&lt;div style=&quot;margin:0 0 1.4em 0;padding:0.95em 1.05em;border:1px solid #d0d7de;border-radius:10px;background:#f6f8fa;&quot;&gt;&lt;p style=&quot;margin:0;font-size:1.02em;line-height:1.45;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;margin:0.35em 0 0 0;color:#57606a;font-size:0.95em;line-height:1.45;&quot;&gt;MisLinux&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is a standalone MisLinux article about a real performance problem I hit while working in large infrastructure repositories with AI coding agents. It reflects my own experience only; I am not affiliated with, sponsored by, or endorsed by Cursor or Anysphere.&lt;/p&gt;

&lt;p&gt;The symptom was subtle at first and painful later:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a repo. Start a chat. Everything feels snappy.&lt;/li&gt;
&lt;li&gt;After a few turns, typing in the chat box gets slower.&lt;/li&gt;
&lt;li&gt;After more turns, even single-character input lags noticeably.&lt;/li&gt;
&lt;li&gt;Activity Monitor / Task Manager / &lt;code&gt;top&lt;/code&gt; shows Cursor helper or indexer processes burning CPU.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pattern looked like model slowness. It was not. It was indexer churn.&lt;/p&gt;

&lt;h2&gt;What was actually happening&lt;/h2&gt;

&lt;p&gt;Cursor (IDE and CLI) indexes the workspace so agents can search and reason about files. That is useful when the indexed files are source code you edit.&lt;/p&gt;

&lt;p&gt;It is harmful when the indexer keeps re-walking directories that never help coding:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Noise directory&lt;/th&gt;&lt;th&gt;Why it hurts&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;history/&lt;/code&gt; and &lt;code&gt;**/history/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Timestamped run artifacts, JSON dumps, screenshots&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;__pycache__/&lt;/code&gt;, &lt;code&gt;.pytest_cache/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Regenerated on every test run&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;node_modules/&lt;/code&gt;, &lt;code&gt;vendor/&lt;/code&gt; (when huge)&lt;/td&gt;&lt;td&gt;Third-party trees&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;*.sqlite&lt;/code&gt;, large &lt;code&gt;*.json&lt;/code&gt;, &lt;code&gt;*.csv&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Operational databases and exports&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Media (&lt;code&gt;*.png&lt;/code&gt;, &lt;code&gt;*.mp4&lt;/code&gt;, …)&lt;/td&gt;&lt;td&gt;Useless for code intelligence, expensive to scan&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;In my ops repo, those paths added up to gigabytes of churn. The chat got slower because the indexer kept working, not because the model suddenly became dumb.&lt;/p&gt;

&lt;h2&gt;The fix: a root &lt;code&gt;.cursorignore&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Git has &lt;code&gt;.gitignore&lt;/code&gt;. Cursor has &lt;code&gt;.cursorignore&lt;/code&gt; at the workspace root (same glob semantics).&lt;/p&gt;

&lt;p&gt;The goal is not to hide code. The goal is to &lt;strong&gt;starve the indexer&lt;/strong&gt; of directories that agents should not treat as editable source.&lt;/p&gt;

&lt;p&gt;Here is the template I use on large monorepos:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# Dependencies and build output
node_modules/
vendor/
dist/
build/
.next/
out/
target/
coverage/
.turbo/
.cache/

# Python/runtime caches
__pycache__/
*.pyc
venv/
.venv/
.pytest_cache/

# Generated / minified assets
*.min.js
*.min.css
*.bundle.js
*.d.ts.map
*.tsbuildinfo

# Operational data (not source)
*.csv
*.json.gz
*.parquet
*.sqlite
*.sql

# Lock files (large, low signal)
package-lock.json
yarn.lock
pnpm-lock.yaml
Cargo.lock

# Media
*.png
*.jpg
*.gif
*.svg
*.mp4
*.woff2

# Repo-specific noise — adapt these
history/
**/history/
operations/**/history/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Three rules that mattered most for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put &lt;code&gt;.cursorignore&lt;/code&gt; at &lt;strong&gt;each repo root&lt;/strong&gt; you open in Cursor (not only a parent folder).&lt;/li&gt;
&lt;li&gt;If you use a monorepo, consider opening the &lt;strong&gt;smallest subdirectory&lt;/strong&gt; that contains the code you edit that day.&lt;/li&gt;
&lt;li&gt;After the first ignore file, restart Cursor once (or trigger reindex) so the exclusion actually applies.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;How I verify the fix worked&lt;/h2&gt;

&lt;p&gt;I use a simple before/after checklist:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Check&lt;/th&gt;&lt;th&gt;Pass criteria&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Character latency&lt;/td&gt;&lt;td&gt;Type in chat for 2 minutes — lag should stay flat, not climb&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CPU&lt;/td&gt;&lt;td&gt;Cursor indexer/helper CPU should drop after reindex settles&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Agent usefulness&lt;/td&gt;&lt;td&gt;Agent can still read the files you care about when explicitly referenced&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If chat is still slow after a good &lt;code&gt;.cursorignore&lt;/code&gt;, the next suspects are: opening too large a folder, an extension fighting the indexer, or a workspace with thousands of small files outside ignore rules.&lt;/p&gt;

&lt;h2&gt;Cursor CLI uses the same contract&lt;/h2&gt;

&lt;p&gt;If you run Cursor Agent from the CLI against a checkout, the same &lt;code&gt;.cursorignore&lt;/code&gt; at that checkout root applies. CLI and IDE share the indexing model.&lt;/p&gt;

&lt;p&gt;Practical CLI habit: &lt;code&gt;cd&lt;/code&gt; into the service directory you are changing, not the entire 50 GB infrastructure tree, unless you truly need cross-repo context.&lt;/p&gt;

&lt;h2&gt;Windows, Linux, and macOS&lt;/h2&gt;

&lt;p&gt;The file name and location are the same on all platforms: &lt;code&gt;.cursorignore&lt;/code&gt; in the project root.&lt;/p&gt;

&lt;p&gt;Path separators inside patterns use forward slashes (Cursor/glob style), even on Windows.&lt;/p&gt;

&lt;p&gt;Typical locations I open:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;OS&lt;/th&gt;&lt;th&gt;Example workspace root&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Linux&lt;/td&gt;&lt;td&gt;&lt;code&gt;/var/www/my-service/&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;macOS&lt;/td&gt;&lt;td&gt;&lt;code&gt;~/Projects/my-service/&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Windows&lt;/td&gt;&lt;td&gt;&lt;code&gt;C:\Users\me\Projects\my-service\&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h2&gt;When &lt;code&gt;.cursorignore&lt;/code&gt; is not enough&lt;/h2&gt;

&lt;p&gt;It fixes indexer noise. It does not fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A chat session that already accumulated a huge context window — start a fresh chat after large investigations.&lt;/li&gt;
&lt;li&gt;Needing files that you ignored — narrow ignores, or &lt;code&gt;@&lt;/code&gt;-mention specific paths.&lt;/li&gt;
&lt;li&gt;Multi-root workspaces where only one root has ignore rules — add &lt;code&gt;.cursorignore&lt;/code&gt; per root.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;My decision rule now&lt;/h2&gt;

&lt;p&gt;Before blaming the model or switching tools, I ask:&lt;/p&gt;

&lt;p&gt;&amp;gt; Is the indexer eating directories that are not source code?&lt;/p&gt;

&lt;p&gt;If yes, I fix &lt;code&gt;.cursorignore&lt;/code&gt; first. That single file has saved me more wall-clock time than any prompt tweak.&lt;/p&gt;

&lt;h2&gt;Related reading on MisLinux&lt;/h2&gt;

&lt;p&gt;If you use AI agents heavily in operations repos, these posts complement this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/03/how-i-use-cir-notes-to-make-ai-coding.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Use CIR Notes to Make AI Coding Agents More Useful&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;Why I Treat Ad-Hoc Scripts Like Real Software&lt;/a&gt; — numbered post 19 in the operations series&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Pace AI-Assisted Operations Work So It Does Not Turn Into a Content Pack&lt;/a&gt; — numbered post 28&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independence note:&lt;/strong&gt; Cursor is a product of Anysphere. This article describes my own troubleshooting workflow and is not official documentation from Cursor.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4816176447333411271'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4816176447333411271'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/how-i-fixed-cursor-getting-slower-every.html' title='How I Fixed Cursor Getting Slower Every Message (Desktop + CLI, Any OS)'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-10388339691445681</id><published>2026-06-16T11:31:27.997+02:00</published><updated>2026-06-16T12:37:16.289+02:00</updated><title type='text'>Business Automation Through AI: How I Turn Manual Chores Into Tested Scripts</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Business Automation Through AI: How I Turn Manual Chores Into Tested Scripts --&gt;
&lt;!-- Suggested labels: AI, Automation, DevOps, Testing, Documentation, Operations, Engineering --&gt;
&lt;!-- Suggested meta description: I do not ask AI to repeat manual work — I ask it to build deterministic scripts, PHPUnit tests, and AGENTS.md CIR notes so the business process survives the next operator. --&gt;
&lt;!-- Cover image source file: html/images/post-31-cover.png --&gt;

&lt;div style=&quot;margin:0 0 1.4em 0;padding:0.95em 1.05em;border:1px solid #d0d7de;border-radius:10px;background:#f6f8fa;&quot;&gt;&lt;p style=&quot;margin:0;font-size:1.02em;line-height:1.45;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;margin:0.35em 0 0 0;color:#57606a;font-size:0.95em;line-height:1.45;&quot;&gt;MisLinux&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It describes how I actually use AI coding agents for business and operations work — not as a chatbot, but as a compiler from messy intent into durable automation.&lt;/p&gt;

&lt;p&gt;Most &amp;quot;AI for business&amp;quot; content stops at drafting emails or summarizing meetings. That is fine. It is also ephemeral.&lt;/p&gt;

&lt;p&gt;The pattern that changed my operations work is different:&lt;/p&gt;

&lt;p&gt;&amp;gt; Every time I catch myself doing a manual chore twice, I ask the AI to implement a &lt;strong&gt;scripted, tested procedure&lt;/strong&gt; — and I capture the nuance in &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Manual intervention becomes the exception. Randomness shrinks.&lt;/p&gt;

&lt;h2&gt;The loop I use&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;1. Do the task once with AI assistance (read-only first)
2. Demand a script/program that encodes the steps
3. Add tests at the strongest safe level (unit → smoke → integration)
4. Record Context / Intent / Rationale in AGENTS.md
5. Save proof artifacts from the first real run
6. Next time: rerun the script, not the conversation&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Step 2 is the discipline. Without it, AI becomes a very fast way to recreate tribal knowledge in chat logs.&lt;/p&gt;

&lt;h2&gt;Example: from spreadsheet panic to finance fill automation&lt;/h2&gt;

&lt;p&gt;Finance month-close used to fail loudly but opaquely: a missing title row, a renamed column, a wrong rev-share cell — and the operator rebuilt context from memory under time pressure.&lt;/p&gt;

&lt;p&gt;The automated path now:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PHP fill libraries&lt;/td&gt;&lt;td&gt;Deterministic column mapping + rev-share math&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CronJob in cluster&lt;/td&gt;&lt;td&gt;Scheduled run with read-only staging inputs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PHPUnit&lt;/td&gt;&lt;td&gt;Locks mapping rules offline&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CIR in AGENTS.md&lt;/td&gt;&lt;td&gt;Explains why Spanish locale uses comma decimals&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Failure email&lt;/td&gt;&lt;td&gt;Includes sheet URL, stage, excerpt, relaunch command&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The AI did not &amp;quot;run finance.&amp;quot; It helped me &lt;strong&gt;extract the invariant rules&lt;/strong&gt; and encode them where regressions are caught before production.&lt;/p&gt;

&lt;h2&gt;Example: publisher onboarding as manifest-driven automation&lt;/h2&gt;

&lt;p&gt;Another class of business work: onboarding a publisher across ad server, dashboards, tags, and internal CMS records.&lt;/p&gt;

&lt;p&gt;The AI helped build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onboard.php --plan / --apply&lt;/code&gt; over YAML manifests&lt;/li&gt;
&lt;li&gt;Readiness checks wired to Telegram on failure only&lt;/li&gt;
&lt;li&gt;Funnel diagnostics (wrapper traffic → ad server → dashboard revenue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each piece has tests. Each non-automatable seam is labeled EXTERNAL in plan output instead of silently skipped.&lt;/p&gt;

&lt;p&gt;That is business automation: the &lt;strong&gt;contract&lt;/strong&gt; is in git, not in someone&amp;#x27;s notebook.&lt;/p&gt;

&lt;h2&gt;Where AGENTS.md and SKILL.md fit&lt;/h2&gt;

&lt;p&gt;Models forget. Repositories should not.&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;File&lt;/th&gt;&lt;th&gt;What it captures&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Context / Intent / Rationale ledger — why the system looks this way&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SKILL.md&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Operator runbook — ordered steps, inputs, safety gates&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Top-of-file CIR in scripts&lt;/td&gt;&lt;td&gt;Why this script exists and what breaks if reverted&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;When a future agent (or human) asks &amp;quot;why is this weird?&amp;quot;, the answer is searchable.&lt;/p&gt;

&lt;p&gt;Good CIR entry shape:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;- Context: what was happening.
  Intent: what we wanted.
  Rationale: why this approach; what we discarded; what re-breaks if reverted.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Without Rationale, the next person &amp;quot;cleans up&amp;quot; the guardrail that prevented yesterday&amp;#x27;s outage.&lt;/p&gt;

&lt;h2&gt;Tests are the guardrails, not decoration&lt;/h2&gt;

&lt;p&gt;I default to PHPUnit in this repo. Python appears where browser CDP is genuinely better — not because a one-off was faster in Python.&lt;/p&gt;

&lt;p&gt;Test layers I aim for on operational code:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Catches&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Unit&lt;/td&gt;&lt;td&gt;Pure logic regressions (parsers, planners, diff engines)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Smoke&lt;/td&gt;&lt;td&gt;CLI wiring, &lt;code&gt;--help&lt;/code&gt;, required args&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fuzzy / property&lt;/td&gt;&lt;td&gt;Random inputs that must not crash or violate invariants&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Integration&lt;/td&gt;&lt;td&gt;Fixture pipelines end-to-end offline&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Gated e2e&lt;/td&gt;&lt;td&gt;Live API/browser only when explicitly enabled&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If a layer is impractical, the blocker goes in AGENTS.md with the nearest safe substitute. &amp;quot;No tests&amp;quot; is not silent.&lt;/p&gt;

&lt;h2&gt;How I prompt AI so the output is durable&lt;/h2&gt;

&lt;p&gt;Weak prompt:&lt;/p&gt;

&lt;p&gt;&amp;gt; Fix the control panel numbers for one publisher row.&lt;/p&gt;

&lt;p&gt;Strong prompt:&lt;/p&gt;

&lt;p&gt;&amp;gt; Read the existing control-panel updater. Add a read-only second pass that filters ad requests using intranet flags. Must be idempotent, PHPUnit-tested, CIR in AGENTS.md, dry-run default, and include exact kubectl job command for one-off proof.&lt;/p&gt;

&lt;p&gt;The second prompt forces &lt;strong&gt;artifacts&lt;/strong&gt; — the things that survive the session.&lt;/p&gt;

&lt;h2&gt;Reducing manual points reduces randomness&lt;/h2&gt;

&lt;p&gt;Every manual step is a slot for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong network selected&lt;/li&gt;
&lt;li&gt;Stale OAuth token&lt;/li&gt;
&lt;li&gt;Spreadsheet column drift&lt;/li&gt;
&lt;li&gt;&amp;quot;I thought we already ran that&amp;quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automation does not mean zero humans. It means humans approve &lt;strong&gt;gates&lt;/strong&gt;, not re-type &lt;strong&gt;data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My practical metric: count manual decisions per run. If a process still needs ten, the next AI session should remove two — with tests.&lt;/p&gt;

&lt;h2&gt;What AI is bad at in business automation&lt;/h2&gt;

&lt;p&gt;Be explicit about limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Economically loaded ad server UI changes without human confirm&lt;/li&gt;
&lt;li&gt;Irreversible production mutations without dry-run proof&lt;/li&gt;
&lt;li&gt;Legal/commercial judgment on contract terms&lt;/li&gt;
&lt;li&gt;Pretending read-only credentials can write because &amp;quot;the script should&amp;quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The manifest/onboard pattern works because it &lt;strong&gt;separates&lt;/strong&gt; check from apply and labels EXTERNAL work honestly.&lt;/p&gt;

&lt;h2&gt;Checklist: is this real automation?&lt;/h2&gt;

&lt;p&gt;Before I merge operational tooling, I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Can a new operator rerun it from &lt;code&gt;--help&lt;/code&gt; output alone?&lt;/li&gt;
&lt;li&gt;[ ] Does dry-run exist and default to safe?&lt;/li&gt;
&lt;li&gt;[ ] Is there at least one automated test locking the core logic?&lt;/li&gt;
&lt;li&gt;[ ] Is there a CIR entry explaining discarded alternatives?&lt;/li&gt;
&lt;li&gt;[ ] Does failure output say what was expected vs what was found?&lt;/li&gt;
&lt;li&gt;[ ] Is there a saved artifact from a successful run?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most boxes are empty, I have a chat transcript — not automation.&lt;/p&gt;

&lt;h2&gt;Tie-in to AI-assisted development hygiene&lt;/h2&gt;

&lt;p&gt;Two sibling posts on this blog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/03/how-i-use-cir-notes-to-make-ai-coding.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Use CIR Notes to Make AI Coding Agents More Useful&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mislinux.blogspot.com/2026/06/how-i-fixed-cursor-getting-slower-every.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;How I Fixed Cursor Getting Slower Every Message&lt;/a&gt; — keep indexer noise out of the repo so agents stay fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Business automation and dev environment hygiene are the same strategy: &lt;strong&gt;make the machine repeat the boring truth&lt;/strong&gt;, and document why the truth looks odd.&lt;/p&gt;

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independence note:&lt;/strong&gt; Tools mentioned (Copilot, Claude, Codex, Cursor, Google Ad Manager, Kubernetes) reflect my stack. No vendor sponsorship implied.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/10388339691445681'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/10388339691445681'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/business-automation-through-ai-how-i.html' title='Business Automation Through AI: How I Turn Manual Chores Into Tested Scripts'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-2256790105430091381</id><published>2026-06-16T11:28:34.626+02:00</published><updated>2026-06-16T12:36:55.619+02:00</updated><title type='text'>Publisher Onboarding as Manifests: How I Automate Ad Server, ads.txt, and Intranet Setup with YAML</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Publisher Onboarding as Manifests: How I Automate Ad Server, ads.txt, and Intranet Setup with YAML --&gt;
&lt;!-- Suggested labels: Automation, DevOps, YAML, Ad Ops, Monitoring, AI, Operations --&gt;
&lt;!-- Suggested meta description: Instead of tribal knowledge spread across nine systems, I describe publisher onboarding as a YAML manifest with plan/apply steps, read-only checks, and monitoring hooks when ad traffic appears. --&gt;
&lt;!-- Cover image source file: html/images/post-30-cover.png --&gt;

&lt;div style=&quot;margin:0 0 1.4em 0;padding:0.95em 1.05em;border:1px solid #d0d7de;border-radius:10px;background:#f6f8fa;&quot;&gt;&lt;p style=&quot;margin:0;font-size:1.02em;line-height:1.45;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;margin:0.35em 0 0 0;color:#57606a;font-size:0.95em;line-height:1.45;&quot;&gt;MisLinux&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It comes from building a real publisher-onboarding pipeline in production, not from a vendor playbook.&lt;/p&gt;

&lt;p&gt;Onboarding a new publisher used to mean a long checklist in someone&amp;#x27;s head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create ad units in the ad server&lt;/li&gt;
&lt;li&gt;Wire demand and pricing floors&lt;/li&gt;
&lt;li&gt;Register the child network and seller ID&lt;/li&gt;
&lt;li&gt;Stand up a revenue dashboard spreadsheet&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;ads.txt&lt;/code&gt; and &lt;code&gt;sellers.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create intranet publisher, sites, wrappers, and contracts&lt;/li&gt;
&lt;li&gt;Send tags to the publisher&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each step touched a different system. Each step had a different tool. And &amp;quot;done&amp;quot; meant different things to different people.&lt;/p&gt;

&lt;p&gt;I fixed that by treating onboarding as &lt;strong&gt;declarative desired state&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;The core idea&lt;/h2&gt;

&lt;p&gt;One YAML file describes what &amp;quot;fully onboarded&amp;quot; means for a publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;publisher: Example Media Co
gam:
  parent_network: &amp;quot;12345678901&amp;quot;
  child_network: &amp;quot;98765432&amp;quot;
  seller_id: pub-example-001
sites:
  - domain: example-news.com
    display_use_domain: true
tags:
  parent_network: &amp;quot;12345678901&amp;quot;
  communication_language: en
stats:
  expect_adunits:
    - Example_news.com-320x50_Display
    - Example_news.com-728x90_Display
    - Example_news.com-PR1_Video
readiness:
  wrappers:
    - uuid: abc123def456-d
  lineitems:
    - name: AdExchange Example Video&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then one orchestrator command compares live state against that manifest:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php onboard.php --config=manifests/example-publisher.onboard.yaml --plan
php onboard.php --config=manifests/example-publisher.onboard.yaml --apply=pcontrol --confirm
php onboard.php --config=manifests/example-publisher.onboard.yaml --apply=all --confirm&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;--plan&lt;/code&gt; is read-only. It prints OK, MISSING, WRONG, or EXTERNAL for every step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--apply&lt;/code&gt; mutates only the steps that are safe to automate. Everything else stays a explicit human gate.&lt;/p&gt;

&lt;p&gt;That split is the whole safety model.&lt;/p&gt;

&lt;h2&gt;Why manifests beat ad-hoc scripts&lt;/h2&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Ad-hoc approach&lt;/th&gt;&lt;th&gt;Manifest approach&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&amp;quot;Run these twelve commands in order&amp;quot;&lt;/td&gt;&lt;td&gt;One file defines end state&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hard to see what is still missing&lt;/td&gt;&lt;td&gt;&lt;code&gt;--plan&lt;/code&gt; is a live diff&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Every new publisher copies paste errors&lt;/td&gt;&lt;td&gt;New publisher = new YAML&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Monitoring is an afterthought&lt;/td&gt;&lt;td&gt;Checks reuse the same manifest&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The manifest is not magic. It is the &lt;strong&gt;contract&lt;/strong&gt; that scripts, tests, and monitoring all read.&lt;/p&gt;

&lt;h2&gt;What one onboarding manifest covers&lt;/h2&gt;

&lt;p&gt;In my pipeline, a full publisher touches roughly nine areas:&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1em 0;&quot;&gt;&lt;tr&gt;&lt;th&gt;Area&lt;/th&gt;&lt;th&gt;Manifest expresses&lt;/th&gt;&lt;th&gt;Automation level&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GAM inventory&lt;/td&gt;&lt;td&gt;Ad unit names, sizes, tiers&lt;/td&gt;&lt;td&gt;Apply where API is safe&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AdExchange demand&lt;/td&gt;&lt;td&gt;Line item names + targeted units&lt;/td&gt;&lt;td&gt;Partial — creative activation may be UI-only&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Placement membership&lt;/td&gt;&lt;td&gt;Union into shared placement&lt;/td&gt;&lt;td&gt;Apply with idempotent sync&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;UPR floors&lt;/td&gt;&lt;td&gt;Expected floor levels vs wrapper&lt;/td&gt;&lt;td&gt;Check against daily export&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MCM child / seller ID&lt;/td&gt;&lt;td&gt;Network codes + seller id&lt;/td&gt;&lt;td&gt;Read-only verify&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Revenue dashboard&lt;/td&gt;&lt;td&gt;Report IDs + sheet templates&lt;/td&gt;&lt;td&gt;Seed + management rows&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ads.txt / sellers.json&lt;/td&gt;&lt;td&gt;Exact lines expected per domain&lt;/td&gt;&lt;td&gt;HTTP fetch + match&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Intranet objects&lt;/td&gt;&lt;td&gt;Company, network, sites, wrappers&lt;/td&gt;&lt;td&gt;DB read checks + controlled writes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Publisher comms&lt;/td&gt;&lt;td&gt;Tag email EN/ES artifacts&lt;/td&gt;&lt;td&gt;Generated from manifest inputs&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Some steps are fully automated. Some are &lt;strong&gt;assisted&lt;/strong&gt; (human in GAM UI, machine verifies after). The manifest makes that honest instead of pretending everything is one button.&lt;/p&gt;

&lt;h2&gt;Checks that can hook into monitoring&lt;/h2&gt;

&lt;p&gt;The same readiness config powers a daily cron:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php publisher-readiness-check.php --config=manifests/example-publisher.onboard.yaml --json&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Gates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrapper serves VAST with expected ad unit in the tag&lt;/li&gt;
&lt;li&gt;Line items are active and target the right inventory&lt;/li&gt;
&lt;li&gt;Control panel rows exist with correct metric flags&lt;/li&gt;
&lt;li&gt;Live &lt;code&gt;ads.txt&lt;/code&gt; contains the expected DIRECT/RESELLER lines&lt;/li&gt;
&lt;li&gt;GAM inventory count matches expectation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a gate fails, Telegram gets an incident message. When everything is green, silence.&lt;/p&gt;

&lt;p&gt;That is the monitoring story the operator asked for: &lt;strong&gt;detect activity and detect missing setup&lt;/strong&gt;, not spam on success.&lt;/p&gt;

&lt;h3&gt;Detecting &amp;quot;onboarding is working&amp;quot; via traffic&lt;/h3&gt;

&lt;p&gt;Separate mode: day-by-day ad request stats per ad unit:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php onboard.php --config=manifests/example-publisher.onboard.yaml --stats=14&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This answers: &amp;quot;We onboarded ten ad units — which ones are actually receiving requests?&amp;quot;&lt;/p&gt;

&lt;p&gt;A roster unit with zero requests is not always failure (publisher has not deployed yet). But once wrapper traffic exists and GAM requests stay zero, the funnel check localizes the break:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php onboard.php --config=manifests/example-publisher.onboard.yaml --funnel --days=7&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wrapper requests → GAM ad unit requests → dashboard revenue. The hop where upstream is non-zero and downstream is zero names the broken layer.&lt;/p&gt;

&lt;h2&gt;AI&amp;#x27;s role: build the procedure, not wing the production click&lt;/h2&gt;

&lt;p&gt;I use AI agents to &lt;strong&gt;author and harden&lt;/strong&gt; the onboarding tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate check libraries and PHPUnit tests&lt;/li&gt;
&lt;li&gt;Record CIR notes when GAM API quirks block automation&lt;/li&gt;
&lt;li&gt;Produce diff output (&lt;code&gt;--diff&lt;/code&gt;) that shows field-level expected vs actual&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not use AI to &amp;quot;just click through GAM&amp;quot; on economically loaded pricing rules without guardrails.&lt;/p&gt;

&lt;p&gt;The manifest makes AI useful because the desired state is explicit. The agent is not guessing what &amp;quot;done&amp;quot; means.&lt;/p&gt;

&lt;h2&gt;What I would not put in a manifest&lt;/h2&gt;

&lt;p&gt;Keep human gates visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating UPR rules that set global floors (no public API — browser workflow with confirm tokens)&lt;/li&gt;
&lt;li&gt;First-time publisher legal review&lt;/li&gt;
&lt;li&gt;Publisher-side &lt;code&gt;ads.txt&lt;/code&gt; edits (we verify, they publish)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trying to hide those behind &lt;code&gt;--apply=all&lt;/code&gt; would create false confidence.&lt;/p&gt;

&lt;h2&gt;Reusable decision rules&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;If it is read-only, automate the check first.&lt;/strong&gt; Production mutation comes later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If two systems must agree (intranet DB vs UI), verify both.&lt;/strong&gt; One path lies sometimes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If a step is EXTERNAL, say so in `--plan`.&lt;/strong&gt; Do not mark WRONG when waiting on the publisher.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store proof artifacts&lt;/strong&gt; next to the manifest run (&lt;code&gt;history/onboarding-emails/...&lt;/code&gt;, activation proof JSON).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Closing thought&lt;/h2&gt;

&lt;p&gt;Publisher onboarding is a business process disguised as nine technical chores.&lt;/p&gt;

&lt;p&gt;YAML manifests do not remove the chores. They make desired state portable, testable, and monitorable — which is what lets AI help without turning production into a roulette wheel.&lt;/p&gt;

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope note:&lt;/strong&gt; Examples use fictional publisher, domain, and network identifiers. The pattern reflects MCM-style ad-ops workflows from my production environment; it is not a case study of any live customer.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2256790105430091381'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2256790105430091381'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/publisher-onboarding-as-manifests-how-i.html' title='Publisher Onboarding as Manifests: How I Automate Ad Server, ads.txt, and Intranet Setup with YAML'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-4870932478200482892</id><published>2026-06-04T11:17:00.000+02:00</published><updated>2026-06-04T11:17:00.120+02:00</updated><title type='text'>How I Turn Read-Only Analysis Into Safer Production Automation</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Turn Read-Only Analysis Into Safer Production Automation --&gt;
&lt;!-- Suggested labels: AI, Automation, DevOps, Production, Reliability, Operations --&gt;
&lt;!-- Suggested meta description: The path I trust most: start with read-only investigation, then shape the mutation workflow around the proof that analysis produced. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is about building safer automation from real operations, not about promising autonomous production.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Read-only to automation cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjuu5V0kjey6IwjFu8NW4NmTdx7mIb-oY0E7zjdPT_7wjREoa5AKOpGgAqsXvCuK9jzUeVsebx8jTAkXJScxQm6Fl-INbQV-p5j-s7GjtrO38v99DKjpDLFTGhyFuIEmnWumjU6EVZxTiO1gl8ryWS-UNUsTEkZyXMdaFMmW0OPZ7sFEF8skN1UMDQrZ5F-&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The automation path I trust most rarely starts with a mutating script.&lt;/p&gt;

&lt;p&gt;It usually starts with read-only analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect the live state&lt;/li&gt;
&lt;li&gt;preserve artifacts&lt;/li&gt;
&lt;li&gt;learn which fields really matter&lt;/li&gt;
&lt;li&gt;discover where the false signals are&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after that do I want to automate the mutation.&lt;/p&gt;

&lt;h2&gt;Why I prefer this order&lt;/h2&gt;

&lt;p&gt;Because the read-only phase tells me:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;What I learn&lt;/th&gt;&lt;th&gt;Why it matters for automation&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;true runtime contract&lt;/td&gt;&lt;td&gt;prevents automating the wrong layer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;trustworthy proof signal&lt;/td&gt;&lt;td&gt;tells me what success should look like&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;rejected paths&lt;/td&gt;&lt;td&gt;stops the next tool from repeating them&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;safe scope boundary&lt;/td&gt;&lt;td&gt;keeps the mutation narrow&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That means the later automation is grounded in reality instead of optimism.&lt;/p&gt;

&lt;h2&gt;The transition I want to see&lt;/h2&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Read-only to automation diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEi2LtFwoIeGxBbSNOxnDQfX80F8-f30zC4r1BaQJSfI_jHzdbKpqnNtVety7An_OX_vMbMUK79o2J1ktPr5sKgWsWYfOrwZ2xTjw-uoNsiuMrxAoG7gN4sEn9TifoZQxJtrFyZYN2FR1QpoEfb0ihHyNN5DFD6L4-QaNEN3lRMRArtbVbaJxtCpeWHhuoTx&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The important thing there is that proof is not an afterthought. Proof is what bridges the investigation into the automation contract.&lt;/p&gt;

&lt;h2&gt;A small workflow example&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# 1. Read-only inspection first
php inspect-current-state.php --out history/20260505-proof/

# 2. Build a proposed mutation from that evidence
php build-change-plan.php --from history/20260505-proof/

# 3. Dry-run the mutation
php apply-change.php --plan history/20260505-proof/plan.json --dry-run

# 4. Apply only once proof + rollback are explicit
php apply-change.php --plan history/20260505-proof/plan.json&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is much safer than starting from step 4 and improvising the rest around it.&lt;/p&gt;

&lt;h2&gt;What I watch for before promoting read-only work into automation&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Is the input shape stable enough?&lt;/li&gt;
&lt;li&gt;Is the output verifiable?&lt;/li&gt;
&lt;li&gt;Can I preserve a before-state?&lt;/li&gt;
&lt;li&gt;Is rollback explicit?&lt;/li&gt;
&lt;li&gt;Do I understand what *not* to automate yet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last question matters a lot. Some workflows should stay partially manual.&lt;/p&gt;

&lt;h2&gt;Failure case: skipping the read-only learning phase&lt;/h2&gt;

&lt;p&gt;If I skip the read-only phase, the first mutation script often makes one of these mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patches the wrong representation&lt;/li&gt;
&lt;li&gt;treats a derived metric as the source of truth&lt;/li&gt;
&lt;li&gt;assumes a browser/UI state that was never actually reached&lt;/li&gt;
&lt;li&gt;writes a partial output where a full-file rewrite is required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not abstract risks. They are exactly the kinds of misunderstandings that production tooling accumulates.&lt;/p&gt;

&lt;h2&gt;Why this matters for AI agents&lt;/h2&gt;

&lt;p&gt;This is probably the most useful pattern I have found for AI-assisted infrastructure work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;let the agent investigate read-only first&lt;/li&gt;
&lt;li&gt;force it to preserve artifacts and explain the contract&lt;/li&gt;
&lt;li&gt;then allow the mutating helper to exist&lt;/li&gt;
&lt;li&gt;keep proof and rollback attached to the same workflow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a much better path than starting with &quot;write the deployment fix.&quot;&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to think read-only analysis was just preparation before the &quot;real&quot; automation work. What I&#39;d do differently now is treat the read-only phase as the place where the automation earns its right to exist.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4870932478200482892'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4870932478200482892'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/how-i-turn-read-only-analysis-into.html' title='How I Turn Read-Only Analysis Into Safer Production Automation'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjuu5V0kjey6IwjFu8NW4NmTdx7mIb-oY0E7zjdPT_7wjREoa5AKOpGgAqsXvCuK9jzUeVsebx8jTAkXJScxQm6Fl-INbQV-p5j-s7GjtrO38v99DKjpDLFTGhyFuIEmnWumjU6EVZxTiO1gl8ryWS-UNUsTEkZyXMdaFMmW0OPZ7sFEF8skN1UMDQrZ5F-=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-8500902532570519798</id><published>2026-06-01T15:41:00.000+02:00</published><updated>2026-06-01T15:41:00.141+02:00</updated><title type='text'>Why Live Follow-Up Files Beat Forgetting the Last Incident</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why Live Follow-Up Files Beat Forgetting the Last Incident --&gt;
&lt;!-- Suggested labels: Operations, Incident Response, Documentation, Follow-up, DevOps, Reliability --&gt;
&lt;!-- Suggested meta description: Why I keep live follow-up files for unresolved operational issues instead of trusting memory, scattered notes, or chat history. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It reflects the follow-up system I now prefer for ongoing operational questions.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Follow-up files cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEixxN-51GGbNsQ-S0tKsVTi-7ExlOeV_yaibG3rS0bjx1mvawjTVHfKt2nCoPqmGyXhGcfxo11W9hznQWucMa00lHsZwXx1KLr6OGe1Fiy4Mb0L4DJq-fcsSRsAhsgyL08OneelTHGfklJkpy8U2FalwBFEILOBEnanWfs-hSoZEcjXu2ZY9w5DQdHNgAsJ&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The incident is not really over just because the page stopped firing.&lt;/p&gt;

&lt;p&gt;Some of the most important work happens after the immediate pressure is gone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify whether the fix actually held&lt;/li&gt;
&lt;li&gt;confirm second-order metrics recovered&lt;/li&gt;
&lt;li&gt;watch for regressions a few days later&lt;/li&gt;
&lt;li&gt;record which questions are still open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I do not keep those as live follow-up items, they turn into vague intentions that disappear under the next urgent task.&lt;/p&gt;

&lt;h2&gt;What I want a follow-up file to contain&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Section&lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Original issue&lt;/td&gt;&lt;td&gt;why this item exists&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Analysis&lt;/td&gt;&lt;td&gt;what the investigation actually found&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Actions taken&lt;/td&gt;&lt;td&gt;what changed already&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Future checks&lt;/td&gt;&lt;td&gt;what still needs to be watched&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Artifact links&lt;/td&gt;&lt;td&gt;where the evidence lives&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That is enough to turn a lingering concern into an inspectable queue item.&lt;/p&gt;

&lt;h2&gt;Why one markdown file is often enough&lt;/h2&gt;

&lt;p&gt;I do not need a huge tracker to get value here.&lt;/p&gt;

&lt;p&gt;One file per follow-up works because it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;easy to diff&lt;/li&gt;
&lt;li&gt;easy to link from a registry page&lt;/li&gt;
&lt;li&gt;easy to archive later&lt;/li&gt;
&lt;li&gt;easy to extend when the story changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What matters is that the follow-up is durable and evidence-backed, not that it looks like an enterprise ticket system.&lt;/p&gt;

&lt;h2&gt;The real advantage&lt;/h2&gt;

&lt;p&gt;The biggest benefit is not organization. It is time.&lt;/p&gt;

&lt;p&gt;When the same topic resurfaces, I want to answer these questions quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what exactly happened last time?&lt;/li&gt;
&lt;li&gt;what did we change already?&lt;/li&gt;
&lt;li&gt;what metric were we supposed to keep watching?&lt;/li&gt;
&lt;li&gt;which artifact folder contains the supporting data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much easier with a live follow-up file than with memory plus old shell output.&lt;/p&gt;

&lt;h2&gt;Failure case: the unresolved item that looks resolved&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes in operational work is treating &quot;the immediate symptom improved&quot; as equivalent to &quot;the issue is closed.&quot;&lt;/p&gt;

&lt;p&gt;That hides important follow-up questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did total demand recover or only one slice?&lt;/li&gt;
&lt;li&gt;did the workaround create a new imbalance?&lt;/li&gt;
&lt;li&gt;did a monitoring threshold now become misleading?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are exactly the questions I want to keep alive explicitly.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to write big postmortems and almost no living follow-up notes. What I&#39;d do differently now is create the follow-up file as soon as I know the issue needs days or weeks of observation, even if the initial analysis is not perfect yet.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8500902532570519798'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8500902532570519798'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/06/why-live-follow-up-files-beat.html' title='Why Live Follow-Up Files Beat Forgetting the Last Incident'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEixxN-51GGbNsQ-S0tKsVTi-7ExlOeV_yaibG3rS0bjx1mvawjTVHfKt2nCoPqmGyXhGcfxo11W9hznQWucMa00lHsZwXx1KLr6OGe1Fiy4Mb0L4DJq-fcsSRsAhsgyL08OneelTHGfklJkpy8U2FalwBFEILOBEnanWfs-hSoZEcjXu2ZY9w5DQdHNgAsJ=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-6728798929106653037</id><published>2026-05-30T10:05:00.000+02:00</published><updated>2026-05-30T10:05:00.118+02:00</updated><title type='text'>How I Decide When an Operator-Facing UI Is Good Enough to Operate From</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Decide When an Operator-Facing UI Is Good Enough to Operate From --&gt;
&lt;!-- Suggested labels: Operations, Analytics, Monitoring, DevOps, Reporting, UI --&gt;
&lt;!-- Suggested meta description: A practical test for operator-facing UIs: a tool is only good enough to operate from when it explains both how to use it and how its numbers or decisions were computed. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is about operator-facing tools in general: spreadsheets, intranet dashboards, web pages, status panels, or any other UI that asks people to make decisions from what it shows.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Google Sheet operations cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgu5QNcNhIQKvpJr8rKdzf2K2AFWWHIW51j7Dt_8InFax_1tptIWLhp1mEKV8__BrevroGKamFlVCfQb1tHNp_b3rO1rdNpcSVG8pyIlg8PFpseOFTyXYdbC-PHT9ppgxF6HeKAlSeQYaVo6j7ciEpthJPDcpOOjB9YcQTk0tS5XicCJIeE0Ra1nTqfDl7W&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;I like operator-facing UIs when they are honest.&lt;/p&gt;

&lt;p&gt;By honest, I mean the sheet tells me two things clearly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;how an operator should use it&lt;/li&gt;
&lt;li&gt;how the important fields were computed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without those two pieces, a dashboard can still look polished while quietly training people to trust unexplained numbers or unexplained statuses.&lt;/p&gt;

&lt;h2&gt;The minimum questions I ask&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Question&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What should I look at first?&lt;/td&gt;&lt;td&gt;reduces wasted scanning time&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What does the status label actually mean?&lt;/td&gt;&lt;td&gt;prevents false confidence&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What is a shortcut metric and what is the real classifier?&lt;/td&gt;&lt;td&gt;stops shallow reading&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Where can I drill into one row?&lt;/td&gt;&lt;td&gt;makes investigation possible&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Where are the thresholds and caveats?&lt;/td&gt;&lt;td&gt;avoids operator folklore&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If the UI cannot answer those questions near the place where people actually use it, I do not think it is ready.&lt;/p&gt;

&lt;h2&gt;The two sections I now prefer&lt;/h2&gt;

&lt;p&gt;I like a dedicated docs panel, inline help surface, nearby runbook, or equivalent split into:&lt;/p&gt;

&lt;h3&gt;Operator guide&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;what the main tab is for&lt;/li&gt;
&lt;li&gt;what to compare first&lt;/li&gt;
&lt;li&gt;where the notes/help live&lt;/li&gt;
&lt;li&gt;where the drill-down tab is&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Inner workings&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;how status is computed&lt;/li&gt;
&lt;li&gt;what each important field means&lt;/li&gt;
&lt;li&gt;what the trailing windows are&lt;/li&gt;
&lt;li&gt;what caveats still exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split matters because most people do not need the formula explanation first thing in the morning, but they do need it during disputes, investigations, or audits.&lt;/p&gt;

&lt;h2&gt;Why comments/notes matter too&lt;/h2&gt;

&lt;p&gt;A good operator-facing UI often needs two layers of explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable documentation close to the tool&lt;/li&gt;
&lt;li&gt;row, field, or state-specific detail near the exact number/status the user is reading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, averages become much more useful when the UI can expose the daily components behind them, and a status badge becomes much more trustworthy when the expert user can still reproduce the exact calculation externally.&lt;/p&gt;

&lt;p&gt;That lets me answer:&lt;/p&gt;

&lt;p&gt;&amp;gt; Was this driven by one spike day or by a sustained shift?&lt;/p&gt;

&lt;p&gt;without leaving the sheet.&lt;/p&gt;

&lt;h2&gt;A simple readiness checklist&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;[ ] operator-facing usage guidance exists in the workbook
[ ] status logic is documented
[ ] key columns are defined
[ ] drill-down links/tabs work
[ ] filters/frozen headers support real operator use
[ ] caveats are written down explicitly&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If two or three of those are missing, I treat the UI as provisional, not operational.&lt;/p&gt;

&lt;h2&gt;Failure case: polished but misleading&lt;/h2&gt;

&lt;p&gt;The failure mode I distrust most is the polished UI that has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a strong-looking status column&lt;/li&gt;
&lt;li&gt;nice formatting&lt;/li&gt;
&lt;li&gt;no explanation of the thresholds&lt;/li&gt;
&lt;li&gt;no path to the underlying detail&lt;/li&gt;
&lt;li&gt;no explanation an expert user can use to reproduce the result externally and validate that the premises still hold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That kind of tool can create faster wrong decisions than a rough but transparent one.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to think operator documentation was a nice-to-have once the formulas or backend logic were correct. What I&#39;d do differently now is design the inexpert-user guide and the expert-user calculation notes as part of the same deliverable as the UI itself, so people can both use the tool and independently verify when it stops deserving trust.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/6728798929106653037'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/6728798929106653037'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/how-i-decide-when-operator-facing-ui-is.html' title='How I Decide When an Operator-Facing UI Is Good Enough to Operate From'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEgu5QNcNhIQKvpJr8rKdzf2K2AFWWHIW51j7Dt_8InFax_1tptIWLhp1mEKV8__BrevroGKamFlVCfQb1tHNp_b3rO1rdNpcSVG8pyIlg8PFpseOFTyXYdbC-PHT9ppgxF6HeKAlSeQYaVo6j7ciEpthJPDcpOOjB9YcQTk0tS5XicCJIeE0Ra1nTqfDl7W=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-8932971777634460815</id><published>2026-05-27T13:41:00.000+02:00</published><updated>2026-05-27T13:41:00.120+02:00</updated><title type='text'>How I Use Proof Artifacts to Keep One-Off Investigations Repeatable</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Use Proof Artifacts to Keep One-Off Investigations Repeatable --&gt;
&lt;!-- Suggested labels: Operations, Incident Response, Automation, Testing, Documentation, DevOps --&gt;
&lt;!-- Suggested meta description: Why I save screenshots, manifests, JSON snapshots, logs, and run notes even for one-off investigations, and how that turns incidents into repeatable workflows. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It reflects my own incident and reporting workflow.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Proof artifacts cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjp1IeTxDMcYjhi7M95hUsx59WbWNrPTb7mAqSF71C_ArWcQHKW8ap46P5cowZKT9RSFNL0dTOyw48_pCf8iR3Quc4oetO0yHQNwUmUMvBJPkp6jJGj-p5YYU02dsQzUiQJup1Vd1SzuvDKnCl8vbYvCB2cykPt_FySzICnALdaWPI_i9RMY59pUQ-XsDlU&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;I no longer believe in the phrase &quot;I already checked that.&quot;&lt;/p&gt;

&lt;p&gt;If I checked it and did not preserve the result, what I really have is memory.&lt;/p&gt;

&lt;p&gt;Memory is useful. It is not an artifact.&lt;/p&gt;

&lt;h2&gt;The artifact types I reach for first&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Artifact&lt;/th&gt;&lt;th&gt;What it proves&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JSON snapshot&lt;/td&gt;&lt;td&gt;machine-readable state at a point in time&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Screenshot&lt;/td&gt;&lt;td&gt;the real UI path or rendered state&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Before/after manifest&lt;/td&gt;&lt;td&gt;configuration delta&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Job log&lt;/td&gt;&lt;td&gt;exact runtime behavior&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Run note&lt;/td&gt;&lt;td&gt;why this artifact mattered&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The run note is important because raw artifacts without framing can become archaeology instead of evidence.&lt;/p&gt;

&lt;h2&gt;Why I save them even for small tasks&lt;/h2&gt;

&lt;p&gt;Because small tasks become big tasks later.&lt;/p&gt;

&lt;p&gt;The one-off investigation often becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the baseline for a later regression&lt;/li&gt;
&lt;li&gt;the model for a new helper script&lt;/li&gt;
&lt;li&gt;the evidence behind a follow-up item&lt;/li&gt;
&lt;li&gt;the reason a CIR note was added&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That only works if the evidence still exists.&lt;/p&gt;

&lt;h2&gt;A folder layout I like&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;history/20260505-example-run/
  summary.json
  before-state.json
  after-state.json
  browser-proof.jpg
  command-log.txt
  run-notes.md&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nothing about that layout is magical. It is just easy to inspect later.&lt;/p&gt;

&lt;h2&gt;The mistake I am trying to avoid&lt;/h2&gt;

&lt;p&gt;The mistake is saving only the &quot;pretty&quot; output.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;saving the chart but not the raw values&lt;/li&gt;
&lt;li&gt;saving the screenshot but not the route or state summary&lt;/li&gt;
&lt;li&gt;saving the report but not the query window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That creates a result I can cite but not re-derive.&lt;/p&gt;

&lt;h2&gt;Why this matters with AI agents&lt;/h2&gt;

&lt;p&gt;AI agents are much more useful when the artifact contract is explicit.&lt;/p&gt;

&lt;p&gt;If the output folder has known files, the next run can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;diff the new result against the old one&lt;/li&gt;
&lt;li&gt;explain what changed&lt;/li&gt;
&lt;li&gt;reuse the same helper path&lt;/li&gt;
&lt;li&gt;avoid re-grepping the whole repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that structure, every investigation starts wide again.&lt;/p&gt;

&lt;h2&gt;A lightweight command trail&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;mkdir -p history/20260505-example-run
php collect-state.php &amp;gt; history/20260505-example-run/before-state.json
python3 take-proof-screenshot.py --out history/20260505-example-run/browser-proof.jpg
printf &#39;%s\n&#39; &quot;Why this run exists&quot; &amp;gt; history/20260505-example-run/run-notes.md&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is enough to make the work reusable.&lt;/p&gt;

&lt;h2&gt;Failure case: artifact-only without explanation&lt;/h2&gt;

&lt;p&gt;Artifacts alone are not enough if they do not say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the operator was trying to prove&lt;/li&gt;
&lt;li&gt;whether the run was read-only or mutating&lt;/li&gt;
&lt;li&gt;why this snapshot matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I still like a small summary file or run note next to the raw files.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to save artifacts only after a task felt important. What I&#39;d do differently now is decide the artifact contract at the start. The earlier I know what proof I want, the less likely I am to finish with missing evidence.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8932971777634460815'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8932971777634460815'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/how-i-use-proof-artifacts-to-keep-one.html' title='How I Use Proof Artifacts to Keep One-Off Investigations Repeatable'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjp1IeTxDMcYjhi7M95hUsx59WbWNrPTb7mAqSF71C_ArWcQHKW8ap46P5cowZKT9RSFNL0dTOyw48_pCf8iR3Quc4oetO0yHQNwUmUMvBJPkp6jJGj-p5YYU02dsQzUiQJup1Vd1SzuvDKnCl8vbYvCB2cykPt_FySzICnALdaWPI_i9RMY59pUQ-XsDlU=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-7892167425445880664</id><published>2026-05-22T17:53:00.000+02:00</published><updated>2026-05-22T17:53:00.136+02:00</updated><title type='text'>What Registry Garbage Collection Actually Costs in Operator Time</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: What Registry Garbage Collection Actually Costs in Operator Time --&gt;
&lt;!-- Suggested labels: Docker, Registry, DevOps, Kubernetes, Operations, Reliability --&gt;
&lt;!-- Suggested meta description: The hidden cost of registry garbage collection is rarely the disk space. It is the operator time spent proving tags, manifests, and blobs are still safe. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is an operator-focused post about self-hosted registry maintenance, not a product review.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Registry GC cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgNarPvBO_ckVQQFDEbvyy4sdCxBQv86fAJ1SMvmV4TLSbt9KZ175ofsw36jVZ1EL_Qq3xqHly_w3-lYg4nYq1AR7ntx-iqBfbIwa8sJU1Ov88u5x9Bq4oaMqjvT-l69icTYUUzx-mXon-yWKWEdwbm0chbhpB7cpyzNcdOkbEQeoziSmlu1gKF86kUA2rM&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When people talk about registry garbage collection, they usually talk about storage.&lt;/p&gt;

&lt;p&gt;I think the more painful cost is operator time.&lt;/p&gt;

&lt;p&gt;Not because cleanup commands are long, but because safe cleanup turns into a proof exercise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which tags are actually still referenced?&lt;/li&gt;
&lt;li&gt;which ones only *look* referenced?&lt;/li&gt;
&lt;li&gt;are the manifest children still readable?&lt;/li&gt;
&lt;li&gt;which tags are part of failover or rollback paths?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The delete is fast. The confidence is expensive.&lt;/p&gt;

&lt;h2&gt;The real work around cleanup&lt;/h2&gt;

&lt;p&gt;This is the table I wish more registry discussions started with:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Phase&lt;/th&gt;&lt;th&gt;Operator work&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Before cleanup&lt;/td&gt;&lt;td&gt;classify runtime vs proof tags, inspect manifests, check active deployments&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;During cleanup&lt;/td&gt;&lt;td&gt;keep the destructive path narrow and explicit&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;After cleanup&lt;/td&gt;&lt;td&gt;prove that kept tags still resolve, pull, and match the intended runtime&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That is why I now think of registry GC as a maintenance workflow, not a command.&lt;/p&gt;

&lt;h2&gt;Why tag existence is not enough&lt;/h2&gt;

&lt;p&gt;One of the nastiest cases in self-hosted registries is the &quot;present but broken&quot; tag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tag still resolves&lt;/li&gt;
&lt;li&gt;the manifest still exists&lt;/li&gt;
&lt;li&gt;one child descriptor or blob is gone&lt;/li&gt;
&lt;li&gt;the next pull fails where nobody expected it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means post-cleanup verification has to be deeper than &quot;the tag name still appears.&quot;&lt;/p&gt;

&lt;h2&gt;The diagram I keep in mind&lt;/h2&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Registry operator cost diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEihQ563czWO66CKLAhtJBhV2YVLlXpB4C4uca9NUNmRgRyKeTaNHmhhg0HwM8mtj34DLuRosc8fzlHbiMPDVSyCNRz3uBZTccA6_MTqFJWzRNlxv64aDG4Xy6xshJFQ4281hfkcQXkUOl1VOJP5D3wSaysIsiHgkQ_EXgAE5JbOHNPMeSCq5An83G2Nt2pc&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The chart uses the cleanup workflow phase on the X axis and a relative operator-effort score on the Y axis. It is not a benchmark. It is a way to show where the real time goes: keep-set classification, manifest/blob proof, and post-cleanup validation.&lt;/p&gt;

&lt;p&gt;The key lesson there is that the operator cost grows with uncertainty about reachability and rollback, not with the number of characters in the command line.&lt;/p&gt;

&lt;h2&gt;The command trail I actually trust&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# 1. List what production and failover still reference
kubectl get deploy,ds,cronjob -A -o json | jq &#39;.items[].spec?&#39;

# 2. Inspect manifest structure, not only tag names
./inspect-registry-manifest.sh runtime-tag --check-blobs

# 3. Run the cleanup only after the keep-set is explicit
registry garbage-collect /etc/distribution/config.yml

# 4. Re-run manifest and pull proof on the kept tags
./inspect-registry-manifest.sh runtime-tag --check-blobs
docker pull registry.example.invalid/app:runtime-tag&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If I skip step 4, I have not actually finished the maintenance.&lt;/p&gt;

&lt;p&gt;Here is what those commands are actually doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubectl get ...&lt;/code&gt; asks Kubernetes which images the live workloads reference right now, so the cleanup keep-set starts from reality instead of memory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inspect-registry-manifest.sh --check-blobs&lt;/code&gt; walks the manifest and child blobs so a tag that merely looks present does not get mistaken for a healthy runtime artifact&lt;/li&gt;
&lt;li&gt;&lt;code&gt;registry garbage-collect ...&lt;/code&gt; is the destructive phase itself&lt;/li&gt;
&lt;li&gt;the final &lt;code&gt;inspect-registry-manifest.sh&lt;/code&gt; plus &lt;code&gt;docker pull&lt;/code&gt; are the proof that the kept runtime path still resolves from the registry all the way down to pullable content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One additional check matters a lot in practice: monitor the exact images and tags the Kubernetes workloads are using, and verify that those tags are still resolvable in the registry. That catches registry drift early, before the next restart turns a hidden integrity problem into a live outage.&lt;/p&gt;

&lt;h2&gt;Failure case: cleanup without path awareness&lt;/h2&gt;

&lt;p&gt;The most dangerous cleanup is not &quot;too aggressive&quot; in the abstract. It is cleanup that does not know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which tags the live cluster uses&lt;/li&gt;
&lt;li&gt;which tags the standby/failover path uses&lt;/li&gt;
&lt;li&gt;which per-architecture descriptors need direct protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how storage maintenance becomes runtime breakage.&lt;/p&gt;

&lt;h2&gt;Why operator time dominates&lt;/h2&gt;

&lt;p&gt;The expensive part is not CPU. It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading the current runtime references&lt;/li&gt;
&lt;li&gt;classifying proof tags versus active tags&lt;/li&gt;
&lt;li&gt;checking manifest reachability&lt;/li&gt;
&lt;li&gt;confirming rollback paths were not silently broken&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is real maintenance work even if the registry disk frees space quickly.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to think of registry GC as a storage task with a verification step. What I&#39;d do differently now is describe it the other way around: it is a verification-heavy operational workflow that happens to reclaim storage at the end.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7892167425445880664'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7892167425445880664'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/what-registry-garbage-collection.html' title='What Registry Garbage Collection Actually Costs in Operator Time'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEgNarPvBO_ckVQQFDEbvyy4sdCxBQv86fAJ1SMvmV4TLSbt9KZ175ofsw36jVZ1EL_Qq3xqHly_w3-lYg4nYq1AR7ntx-iqBfbIwa8sJU1Ov88u5x9Bq4oaMqjvT-l69icTYUUzx-mXon-yWKWEdwbm0chbhpB7cpyzNcdOkbEQeoziSmlu1gKF86kUA2rM=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-5127341469132699707</id><published>2026-05-17T17:05:00.000+02:00</published><updated>2026-05-17T17:05:00.120+02:00</updated><title type='text'>Why Monitoring Needs Host, Hosting, and Global Views at the Same Time</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why Monitoring Needs Host, Hosting, and Global Views at the Same Time --&gt;
&lt;!-- Suggested labels: Monitoring, DevOps, Observability, Kubernetes, Infrastructure, Operations --&gt;
&lt;!-- Suggested meta description: A monitoring lesson I learned the hard way: host-level, hosting-level, and global views answer different failure questions, and you need all three. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is based on real operational monitoring design work, not a generic observability checklist.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Monitoring scope cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjL51kswaAy-x1TKcL-wLbOXvMBH-xqP4FQUGGzOJko7ziVQIIZa7LfU_WECh3VOjzmjBRIOryIATEjk_TJ4gbnOSUQrxo-i4t5V_AJMAiS_ysSlpa4NsehBNJw6gqnwNB1w6lChtIdtF9xjHWoWf8Sx2RexLOErCxwiRd7qqqA3wQHiouJrueIeKD6oU3v&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;One of the easiest ways to build a misleading dashboard is to choose only one monitoring scope.&lt;/p&gt;

&lt;p&gt;If I watch only the global service, I can miss a saturated origin.&lt;/p&gt;

&lt;p&gt;If I watch only the individual host, I can page on a healthy rebalance.&lt;/p&gt;

&lt;p&gt;If I watch only the provider slice, I can miss a cross-provider traffic cliff.&lt;/p&gt;

&lt;p&gt;That is why I now want three views at the same time:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;View&lt;/th&gt;&lt;th&gt;Main question&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Host&lt;/td&gt;&lt;td&gt;Is one concrete machine or origin sick, stale, or overloaded?&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hosting&lt;/td&gt;&lt;td&gt;Is one provider/location carrying the wrong share or drifting operationally?&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Global&lt;/td&gt;&lt;td&gt;Is the public service really up, healthy, and serving normal demand overall?&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h2&gt;What each level catches&lt;/h2&gt;

&lt;h3&gt;Host view&lt;/h3&gt;

&lt;p&gt;The host view is where I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;CPU or saturation signals&lt;/li&gt;
&lt;li&gt;config freshness&lt;/li&gt;
&lt;li&gt;health endpoint state&lt;/li&gt;
&lt;li&gt;reload or restart anomalies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how I catch &quot;one box is bad&quot; before it becomes &quot;the system feels random.&quot;&lt;/p&gt;

&lt;h3&gt;Hosting view&lt;/h3&gt;

&lt;p&gt;The hosting view groups hosts by provider or operational domain.&lt;/p&gt;

&lt;p&gt;That is where I can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traffic imbalance&lt;/li&gt;
&lt;li&gt;one provider falling behind on config freshness&lt;/li&gt;
&lt;li&gt;one provider serving the wrong share of requests&lt;/li&gt;
&lt;li&gt;one provider drifting in latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters even if there is only one host in that hosting today, because the grouping is an operational concept, not just a host count.&lt;/p&gt;

&lt;h3&gt;Global view&lt;/h3&gt;

&lt;p&gt;The global view answers the question that users care about:&lt;/p&gt;

&lt;p&gt;&amp;gt; Is the public service healthy overall?&lt;/p&gt;

&lt;p&gt;That is the view where I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total public traffic&lt;/li&gt;
&lt;li&gt;total error rate&lt;/li&gt;
&lt;li&gt;canary health across all active public origins&lt;/li&gt;
&lt;li&gt;global per-UUID or per-surface demand cliffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this layer, it is too easy to confuse a redistribution event with a real outage.&lt;/p&gt;

&lt;h2&gt;The architecture I prefer&lt;/h2&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Monitoring scope diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhjI-w64i41BfqWsbgDzW-fQKsG7oYHnovCW2kwUp3uizDWVhvFDJ3HiOB_j5gF-uePjBT--5pQ7w9v5k99d7A1SdBRMG_znDLNre2tROihi1SDgco1OWxintSdpE93rdDc8QBM22QJJflzI8X9cHiO4qDqJZ1v5jOHoXuuCQ1R-N3SUVHF6Ds55HPcOlyV&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The point of that diagram is that each level aggregates differently. They are not interchangeable charts with different legends.&lt;/p&gt;

&lt;h2&gt;A practical example&lt;/h2&gt;

&lt;p&gt;Imagine public traffic is split across two providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider A carries less traffic than usual&lt;/li&gt;
&lt;li&gt;provider B carries more traffic than usual&lt;/li&gt;
&lt;li&gt;the total public request rate is stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I alert only on provider A, I page on a healthy balance shift.&lt;/p&gt;

&lt;p&gt;If I alert only on global traffic, I miss the provider imbalance.&lt;/p&gt;

&lt;p&gt;If I alert only on per-host CPU, I may not notice that one provider is now becoming the hidden bottleneck.&lt;/p&gt;

&lt;p&gt;The right answer is to keep all three views and interpret them together.&lt;/p&gt;

&lt;h2&gt;Metrics I now want at each level&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Scope&lt;/th&gt;&lt;th&gt;Metrics I care about first&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Host&lt;/td&gt;&lt;td&gt;health, latency, CPU, config age, recent reload status&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hosting&lt;/td&gt;&lt;td&gt;total requests, mean latency, error rate, freshness spread, host count&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Global&lt;/td&gt;&lt;td&gt;total public requests, total errors, demand cliffs, public canary result&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That table looks obvious after the fact, but it took me longer than it should have to stop flattening these scopes into one graph.&lt;/p&gt;

&lt;h2&gt;Failure case: the wrong dashboard story&lt;/h2&gt;

&lt;p&gt;The wrong dashboard story sounds like this:&lt;/p&gt;

&lt;p&gt;&amp;gt; &quot;Traffic is down on one origin, so the public service is down.&quot;&lt;/p&gt;

&lt;p&gt;That sentence mixes scopes. It takes a host observation and turns it into a global conclusion.&lt;/p&gt;

&lt;p&gt;Once I noticed that pattern, a lot of monitoring noise suddenly made more sense.&lt;/p&gt;

&lt;h2&gt;Command trail I want during validation&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# Host-level health
curl -s https://origin-a.example.invalid/health

# Hosting-level request split
python3 inspect-request-split.py --group-by hosting

# Global service trend
python3 inspect-request-split.py --group-by global&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The exact implementation can vary. The important part is that the queries do not collapse distinct operational questions into one series.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to think multiple scopes were a nice refinement for later. I now think they are the minimum design for any public service that can move traffic across origins or providers. If I were rebuilding the monitoring from scratch, I would model host, hosting, and global identity from day one.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/5127341469132699707'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/5127341469132699707'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/why-monitoring-needs-host-hosting-and.html' title='Why Monitoring Needs Host, Hosting, and Global Views at the Same Time'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjL51kswaAy-x1TKcL-wLbOXvMBH-xqP4FQUGGzOJko7ziVQIIZa7LfU_WECh3VOjzmjBRIOryIATEjk_TJ4gbnOSUQrxo-i4t5V_AJMAiS_ysSlpa4NsehBNJw6gqnwNB1w6lChtIdtF9xjHWoWf8Sx2RexLOErCxwiRd7qqqA3wQHiouJrueIeKD6oU3v=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-698486167515037386</id><published>2026-05-14T16:05:00.001+02:00</published><updated>2026-05-14T16:05:00.120+02:00</updated><title type='text'>What a Web AI Automation Blocker Really Costs During an Investigation</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: What a Web AI Automation Blocker Really Costs During an Investigation --&gt;
&lt;!-- Suggested labels: Browser Automation, DevOps, Incident Response, AI, Testing, Operations --&gt;
&lt;!-- Suggested meta description: Browser automation blockers are not just annoying. They distort timelines, proofs, and operator confidence. Here is how I now treat them. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is based on real operational browser work for web AI automation and adjacent browser-driven investigation paths, not on demo-only automation.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Browser automation blocker cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhc3d48awvFdWniYiBZuHtlrije9uEzV-fEASGrVK5mfPfujiLE45ivPJCOO6tBFr-sqXI0_HMUJpEDVdRmtvjmqCrbEh235WCewsx6MsUwKtsXTZ0MQlrmlTHi7qZpxL5fMUXggu5CiZoNv71v50BpMG9HpSmnDi0gI3biUCxdohoYQMZ9KcRx35i1Ce6m&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When a browser automation task gets blocked by login friction, bot protection, or a challenge page, the obvious cost is time.&lt;/p&gt;

&lt;p&gt;The less obvious cost is decision quality.&lt;/p&gt;

&lt;p&gt;Once the clean automated path is blocked, people often start improvising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one manual browser for the challenge&lt;/li&gt;
&lt;li&gt;another browser for the actual task&lt;/li&gt;
&lt;li&gt;partial screenshots&lt;/li&gt;
&lt;li&gt;copied HTML from the wrong frame&lt;/li&gt;
&lt;li&gt;assumptions about what would have happened after the challenge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how proof quality degrades.&lt;/p&gt;

&lt;h2&gt;Why this matters&lt;/h2&gt;

&lt;p&gt;During an investigation, a browser path is often being used to answer one of these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what does the real UI show right now?&lt;/li&gt;
&lt;li&gt;which exact button or dialog state matters?&lt;/li&gt;
&lt;li&gt;is the saved HTML the same as the rendered state?&lt;/li&gt;
&lt;li&gt;which route fragment is still valid?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the automation path is blocked, the investigation has to account for that blocker explicitly instead of pretending the missing evidence is equivalent to a negative result.&lt;/p&gt;

&lt;h2&gt;The three states I distinguish now&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;State&lt;/th&gt;&lt;th&gt;What it means&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Automated success&lt;/td&gt;&lt;td&gt;The browser reached the intended DOM/runtime path and the evidence is complete&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Manual bridge needed&lt;/td&gt;&lt;td&gt;A human has to clear a gate, then automation may resume&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hard blocker&lt;/td&gt;&lt;td&gt;The session cannot proceed safely or repeatably right now&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That middle state matters a lot. Many investigations are not &quot;blocked forever.&quot; They are &quot;blocked until a human clears exactly one step.&quot;&lt;/p&gt;

&lt;h2&gt;What I do instead of brute forcing it&lt;/h2&gt;

&lt;p&gt;My preferred sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;preserve the blocker evidence&lt;/li&gt;
&lt;li&gt;launch a separate, clearly scoped headed session if needed&lt;/li&gt;
&lt;li&gt;let the human clear the gate&lt;/li&gt;
&lt;li&gt;resume automation from the now-authenticated session&lt;/li&gt;
&lt;li&gt;save artifacts from the real post-gate state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps the proof chain cleaner than mixing one-off manual browsing with later guesswork.&lt;/p&gt;

&lt;h2&gt;A command trail that reflects that model&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# 1. Launch a separate headed session for the blocked surface
python3 ensure-browser-session.py --port 9230 --headed --target-url https://example.invalid

# 2. Record the blocker artifact first
python3 capture-blocked-state.py --port 9230 --output history/blocked-proof/

# 3. After the manual gate is cleared, resume the structured automation
python3 collect-real-ui-proof.py --port 9230 --output history/post-gate-proof/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The important part is not the exact script names. The important part is the explicit handoff.&lt;/p&gt;

&lt;p&gt;In plain language, those example commands do three different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ensure-browser-session.py&lt;/code&gt; starts a separate browser profile on its own debugging port so the blocked flow does not contaminate the main session&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capture-blocked-state.py&lt;/code&gt; saves the proof that the current stop point is the challenge page rather than the real application DOM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;collect-real-ui-proof.py&lt;/code&gt; resumes structured inspection after the human clears the blocker, so the later screenshots/HTML/runtime notes all come from the real target state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The filenames here are illustrative. The reusable rule is what matters: separate the blocked session, preserve evidence of the blocker, and only then resume the automated collection path.&lt;/p&gt;

&lt;h2&gt;Failure case: what goes wrong if I skip the handoff model&lt;/h2&gt;

&lt;p&gt;If I skip that structure, I get mixed evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;screenshots from one session&lt;/li&gt;
&lt;li&gt;HTML from another&lt;/li&gt;
&lt;li&gt;cookies in a third&lt;/li&gt;
&lt;li&gt;conclusions that assume all of them refer to the same state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is especially dangerous when the system under investigation has expiring auth or anti-bot checks.&lt;/p&gt;

&lt;h2&gt;Why this matters for AI-assisted work&lt;/h2&gt;

&lt;p&gt;AI agents are very good at continuing confidently after a blocker page.&lt;/p&gt;

&lt;p&gt;That is exactly why I want the blocker to be first-class in the workflow.&lt;/p&gt;

&lt;p&gt;The right conclusion is often:&lt;/p&gt;

&lt;p&gt;&amp;gt; &quot;I do not have the actual runtime DOM yet. I only have the blocker state.&quot;&lt;/p&gt;

&lt;p&gt;That is a useful result, but it is not the same result as a successful DOM inspection.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to treat browser blockers as an inconvenience outside the &quot;real&quot; work. What I&#39;d do differently now is design the browser workflow around them from the start: separate session, explicit handoff, and artifact-backed resumption. It turns a messy interruption into a real operator state.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/698486167515037386'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/698486167515037386'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/what-web-ai-automation-blocker-really.html' title='What a Web AI Automation Blocker Really Costs During an Investigation'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEhc3d48awvFdWniYiBZuHtlrije9uEzV-fEASGrVK5mfPfujiLE45ivPJCOO6tBFr-sqXI0_HMUJpEDVdRmtvjmqCrbEh235WCewsx6MsUwKtsXTZ0MQlrmlTHi7qZpxL5fMUXggu5CiZoNv71v50BpMG9HpSmnDi0gI3biUCxdohoYQMZ9KcRx35i1Ce6m=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-5742889600334814454</id><published>2026-05-12T15:05:00.001+02:00</published><updated>2026-05-12T15:05:00.117+02:00</updated><title type='text'>Why I Treat Ad-Hoc Scripts Like Real Software</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why I Treat Ad-Hoc Scripts Like Real Software --&gt;
&lt;!-- Suggested labels: Automation, DevOps, Scripting, Testing, AI, Operations --&gt;
&lt;!-- Suggested meta description: The habit that changed my infrastructure work: even one-off scripts should be DRY, tested, and documented like real source code. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It comes from repeated operator work, not from a vendor playbook.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Ad-hoc scripts cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiMAsSNze6i3obxES9wW1yoW39BC_HBk7bY1KH3R65EDpkzcRLkLtykX1ww513cRU7al1bjRElmTk2sl3eOFRG2aYvfIegnWbTwi_buY1m_HhiVhHi4DaT-Zg_6aPdPr5IXfJWU5ZdMQ7nJtIfkuLzu81Eb__9j4J25PbqjHPDojGcSb8RFjhX2iEicpnkI&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;I used to use the phrase &quot;just a quick script&quot; as if it reduced the risk.&lt;/p&gt;

&lt;p&gt;It did not.&lt;/p&gt;

&lt;p&gt;The script still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;touched real data&lt;/li&gt;
&lt;li&gt;encoded assumptions&lt;/li&gt;
&lt;li&gt;created artifacts someone would rely on later&lt;/li&gt;
&lt;li&gt;became copy-paste material for the next incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point I stopped calling these scripts disposable. I started treating them as source code that merely had a short original deadline.&lt;/p&gt;

&lt;h2&gt;The rule I use now&lt;/h2&gt;

&lt;p&gt;If a script is important enough to run once against real operational data, it is important enough to deserve:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Minimum standard&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DRY helpers&lt;/td&gt;&lt;td&gt;avoid re-deriving parsing and transport logic later&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;tests&lt;/td&gt;&lt;td&gt;catch the obvious contract breaks before the next rerun&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CIR notes&lt;/td&gt;&lt;td&gt;preserve why the workflow exists in this shape&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;saved artifacts&lt;/td&gt;&lt;td&gt;prove what happened on that run&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That does not mean every script needs a huge framework. It means the script should stop pretending it is exempt from engineering standards.&lt;/p&gt;

&lt;h2&gt;What changed my mind&lt;/h2&gt;

&lt;p&gt;The real problem was not the first run. The first run usually works well enough.&lt;/p&gt;

&lt;p&gt;The problem was the second and third run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slightly different data&lt;/li&gt;
&lt;li&gt;slightly different date window&lt;/li&gt;
&lt;li&gt;slightly different production state&lt;/li&gt;
&lt;li&gt;slightly different operator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where &quot;ad hoc&quot; turns into &quot;mysterious legacy.&quot;&lt;/p&gt;

&lt;h2&gt;A script can be short and still be real code&lt;/h2&gt;

&lt;p&gt;I do not need a large package structure before I apply discipline.&lt;/p&gt;

&lt;p&gt;Even a small automation helper can be structured like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;script entrypoint
  -&amp;gt; parse arguments
  -&amp;gt; call a shared library/helper
  -&amp;gt; write artifacts
  -&amp;gt; exit non-zero on real failure&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That shape makes testing possible and keeps the second script from re-implementing the same logic a week later.&lt;/p&gt;

&lt;h2&gt;A concrete command trail I prefer&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# Good: deterministic rerun with explicit output folder
php operations/example/report.php \
  --input artifacts/source.json \
  --output history/20260505-example-run/

# Good: test the shared logic separately
vendor/bin/phpunit tests/example/ReportBuilderTest.php

# Better: use the exact same helper from the next automation layer
php operations/example/next-stage-proof.php --from history/20260505-example-run/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is much safer than three unrelated shell fragments that happen to work today.&lt;/p&gt;

&lt;h2&gt;The subtle benefit: better failure messages&lt;/h2&gt;

&lt;p&gt;Once I treat the script like real software, I also stop accepting vague operator output.&lt;/p&gt;

&lt;p&gt;I want errors that explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what input was missing&lt;/li&gt;
&lt;li&gt;what upstream state was unexpected&lt;/li&gt;
&lt;li&gt;which artifact path contains the evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters more than people think. A one-off script with useless errors becomes a slow manual process again the moment it fails.&lt;/p&gt;

&lt;h2&gt;Failure case: the fake one-off&lt;/h2&gt;

&lt;p&gt;The fake one-off script usually looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one file&lt;/li&gt;
&lt;li&gt;mixed fetching, parsing, mutation, and reporting&lt;/li&gt;
&lt;li&gt;hardcoded ids or paths&lt;/li&gt;
&lt;li&gt;no test seam&lt;/li&gt;
&lt;li&gt;no artifact contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then somebody asks for the same run with a new date range or a new target, and now it has to be reverse-engineered from scratch.&lt;/p&gt;

&lt;h2&gt;Why this matters more with AI-assisted workflows&lt;/h2&gt;

&lt;p&gt;AI agents are extremely good at making a one-off script &quot;work.&quot;&lt;/p&gt;

&lt;p&gt;They are much less useful if the script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hides its assumptions&lt;/li&gt;
&lt;li&gt;cannot be re-run deterministically&lt;/li&gt;
&lt;li&gt;has no tests&lt;/li&gt;
&lt;li&gt;has no saved evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I now want the script to be reusable before I want it to be clever.&lt;/p&gt;

&lt;h2&gt;My current checklist for a so-called ad-hoc script&lt;/h2&gt;

&lt;p&gt;Before I accept it, I want:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a small shared library if parsing or transport logic may recur&lt;/li&gt;
&lt;li&gt;at least one unit test around the risky part&lt;/li&gt;
&lt;li&gt;an output directory or artifact contract&lt;/li&gt;
&lt;li&gt;one CIR note explaining why the script exists and what would go wrong if someone &quot;simplified&quot; it blindly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it touches production directly, I want even more proof than that.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to optimize for speed on the first run and structure on the second run. What I would do differently now is merge those decisions: build the smallest reusable version first. It is usually only slightly slower at the beginning and much faster after that.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/5742889600334814454'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/5742889600334814454'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/why-i-treat-ad-hoc-scripts-like-real.html' title='Why I Treat Ad-Hoc Scripts Like Real Software'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEiMAsSNze6i3obxES9wW1yoW39BC_HBk7bY1KH3R65EDpkzcRLkLtykX1ww513cRU7al1bjRElmTk2sl3eOFRG2aYvfIegnWbTwi_buY1m_HhiVhHi4DaT-Zg_6aPdPr5IXfJWU5ZdMQ7nJtIfkuLzu81Eb__9j4J25PbqjHPDojGcSb8RFjhX2iEicpnkI=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-7447289456094056709</id><published>2026-05-08T17:53:00.011+02:00</published><updated>2026-05-08T17:53:00.132+02:00</updated><title type='text'>How I Decide Whether an AI Agent Is Allowed to Touch Production</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Decide Whether an AI Agent Is Allowed to Touch Production --&gt;
&lt;!-- Suggested labels: AI, GitHub Copilot, Automation, DevOps, Production, CIR --&gt;
&lt;!-- Suggested meta description: A practical framework for deciding when an AI coding agent can inspect, modify, or deploy production-adjacent systems without turning automation into blind trust. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is a standalone MisLinux article about production safety and AI-assisted operations. It reflects my own working rules and mistakes. I am not affiliated with GitHub, OpenAI, Anthropic, or any other AI vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;AI agent production safety cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjOhllMW32JELARNKJ1JRqwQ1VSnQf7OnslO8msW1iuoGZy79FSOJpguTg1dIZM5NnRwAj-KEegNoNbQo74pVDHEMqRFoYLNcjEDy8tUtJonqSeXlfTjZTesyI1nFB0K6g6eUJ361PEHMjmprnUW6Op8nSX87y7iTxjJcydTU-c7eRYl7vSCThCqJlfqGqN&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;I like AI coding agents, but I do not think &quot;the agent can do it&quot; is a deployment policy.&lt;/p&gt;

&lt;p&gt;The question I care about is more specific:&lt;/p&gt;

&lt;p&gt;&amp;gt; Under what conditions is an AI agent allowed to inspect, modify, or deploy something that touches production?&lt;/p&gt;

&lt;p&gt;That answer should not depend on how impressive the last demo was. It should depend on the shape of the task, the reversibility of the action, the quality of the documentation, and the proof I expect before the change becomes real.&lt;/p&gt;

&lt;h2&gt;My default assumption&lt;/h2&gt;

&lt;p&gt;My default assumption is that an AI agent is good at speed, pattern matching, and synthesis, but dangerous when context is ambiguous and the blast radius is large.&lt;/p&gt;

&lt;p&gt;That means I am comfortable letting an agent do more when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the task is well-bounded&lt;/li&gt;
&lt;li&gt;the workflow is reproducible&lt;/li&gt;
&lt;li&gt;the expected output can be validated clearly&lt;/li&gt;
&lt;li&gt;rollback is easy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I get much more conservative when any of those stop being true.&lt;/p&gt;

&lt;h2&gt;The first filter: read-only or mutating?&lt;/h2&gt;

&lt;p&gt;The first question I ask is whether the task is read-only or mutating.&lt;/p&gt;

&lt;p&gt;Read-only work is the easiest place to gain value safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;investigate logs&lt;/li&gt;
&lt;li&gt;compare manifests&lt;/li&gt;
&lt;li&gt;trace where a metric comes from&lt;/li&gt;
&lt;li&gt;generate a report&lt;/li&gt;
&lt;li&gt;audit what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mutation is a different category:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editing a production manifest&lt;/li&gt;
&lt;li&gt;publishing a new image&lt;/li&gt;
&lt;li&gt;updating an ad server or video player config&lt;/li&gt;
&lt;li&gt;changing a live Blogger theme&lt;/li&gt;
&lt;li&gt;switching a mail relay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not treat those categories as morally different. I treat them as risk-different.&lt;/p&gt;

&lt;h2&gt;The second filter: can the agent explain the exact runtime contract?&lt;/h2&gt;

&lt;p&gt;One of my strongest rules is that I want the system to be documented well enough that the agent can explain what the change actually does, not merely what line it changed.&lt;/p&gt;

&lt;p&gt;That is one reason I use Context / Intent / Rationale notes near the code and configuration. Good CIR is not there to make the repo look organized. It is there so the next change is grounded in the reason the current behavior exists.&lt;/p&gt;

&lt;p&gt;If an agent cannot explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why the current behavior was chosen&lt;/li&gt;
&lt;li&gt;what would break if it is reverted&lt;/li&gt;
&lt;li&gt;which constraints shaped the implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then I do not think it is ready to mutate that area safely.&lt;/p&gt;

&lt;h2&gt;The third filter: can I demand proof, not confidence?&lt;/h2&gt;

&lt;p&gt;This is the rule that matters most in practice.&lt;/p&gt;

&lt;p&gt;I do not care whether the agent sounds confident. I care whether it can produce the right proof for the task.&lt;/p&gt;

&lt;p&gt;The proof depends on the task:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Task type&lt;/th&gt;&lt;th&gt;Minimum proof I want&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Code-only local change&lt;/td&gt;&lt;td&gt;tests and a reproducible output or diff&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Build/publish change&lt;/td&gt;&lt;td&gt;successful image build plus inspection of what was published&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Production config mutation&lt;/td&gt;&lt;td&gt;before/after state, live status proof, and rollback artifact&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Browser/UI automation&lt;/td&gt;&lt;td&gt;saved screenshots or structured artifacts from the real UI path&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Report generation&lt;/td&gt;&lt;td&gt;saved raw inputs plus derived summary output&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This is where a lot of AI-agent workflows fail. They substitute explanation for proof. I want both, but if I have to choose, proof matters more.&lt;/p&gt;

&lt;h2&gt;The fourth filter: how reversible is the action?&lt;/h2&gt;

&lt;p&gt;The easiest production mutations to trust are the ones with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit backups&lt;/li&gt;
&lt;li&gt;narrow scope&lt;/li&gt;
&lt;li&gt;rollback instructions&lt;/li&gt;
&lt;li&gt;separate staging or test path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest ones are the ones where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the live system is the only place to validate behavior&lt;/li&gt;
&lt;li&gt;the previous state was not captured&lt;/li&gt;
&lt;li&gt;rollback depends on memory or luck&lt;/li&gt;
&lt;li&gt;the action affects several systems at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I am much happier letting an agent update a test DaemonSet than directly change the only public &lt;code&gt;hostPort&lt;/code&gt; DaemonSet serving real traffic.&lt;/p&gt;

&lt;h2&gt;The fifth filter: can the task be serialized?&lt;/h2&gt;

&lt;p&gt;One subtle production risk with AI agents is concurrency.&lt;/p&gt;

&lt;p&gt;An agent can work very fast and in parallel, but some systems should not be mutated in parallel even if parallelism is technically possible. I care about that especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live browser editors&lt;/li&gt;
&lt;li&gt;production registry maintenance&lt;/li&gt;
&lt;li&gt;ad server and video player config changes on the same object&lt;/li&gt;
&lt;li&gt;migration flows where one save can overwrite another&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So another rule I use is: the agent is allowed to move fast only where the workflow is safe to parallelize. Otherwise, the correct behavior is serialization.&lt;/p&gt;

&lt;h2&gt;What I let agents do comfortably today&lt;/h2&gt;

&lt;p&gt;I am fairly comfortable letting an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;investigate a live issue in read-only mode&lt;/li&gt;
&lt;li&gt;prepare a deterministic config change with backups and proof steps&lt;/li&gt;
&lt;li&gt;update automation or tests in the repo&lt;/li&gt;
&lt;li&gt;generate reports, charts, or artifacts from saved data&lt;/li&gt;
&lt;li&gt;perform staged mutations when the rollback and validation path is already documented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot of useful work. It is also very different from saying &quot;the agent can just operate production.&quot;&lt;/p&gt;

&lt;h2&gt;What I still treat cautiously&lt;/h2&gt;

&lt;p&gt;I get much more cautious when the task involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ambiguous business logic&lt;/li&gt;
&lt;li&gt;undocumented production behavior&lt;/li&gt;
&lt;li&gt;irreversible live changes&lt;/li&gt;
&lt;li&gt;auth/session-sensitive browser flows&lt;/li&gt;
&lt;li&gt;destructive cleanup where the real state graph is not fully known&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the areas where the agent can still help, but usually as a prepared operator rather than an unsupervised one.&lt;/p&gt;

&lt;h2&gt;The working rule I come back to&lt;/h2&gt;

&lt;p&gt;My practical rule is this:&lt;/p&gt;

&lt;p&gt;&amp;gt; An AI agent is allowed to touch production only when the task is documented well enough, bounded enough, and verifiable enough that the agent is not replacing judgment — it is executing a judgment-backed workflow.&lt;/p&gt;

&lt;p&gt;That distinction matters a lot.&lt;/p&gt;

&lt;p&gt;The goal is not to create a magical autonomous operator. The goal is to turn documented operational knowledge into a faster, more repeatable execution path without lowering the safety bar.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;The most dangerous mistake with AI agents is not overestimating their code skills. It is underestimating how much production safety depends on context, reversibility, and proof.&lt;/p&gt;

&lt;p&gt;That is why my trust model is not based on whether the agent &quot;feels smart.&quot; It is based on whether the workflow itself deserves automation.&lt;/p&gt;

&lt;p&gt;If the system is well-documented, the action is bounded, the evidence is explicit, and rollback is real, I am happy to let the agent do a lot. If those things are missing, the right answer is not &quot;trust the model more.&quot; The right answer is to improve the workflow first.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7447289456094056709'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7447289456094056709'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/how-i-decide-whether-ai-agent-is.html' title='How I Decide Whether an AI Agent Is Allowed to Touch Production'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjOhllMW32JELARNKJ1JRqwQ1VSnQf7OnslO8msW1iuoGZy79FSOJpguTg1dIZM5NnRwAj-KEegNoNbQo74pVDHEMqRFoYLNcjEDy8tUtJonqSeXlfTjZTesyI1nFB0K6g6eUJ361PEHMjmprnUW6Op8nSX87y7iTxjJcydTU-c7eRYl7vSCThCqJlfqGqN=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-1073217737204446139</id><published>2026-05-08T12:17:00.001+02:00</published><updated>2026-05-08T12:17:00.121+02:00</updated><title type='text'>Why I Log Context Before I Touch Anything Live</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why I Log Context Before I Touch Anything Live --&gt;
&lt;!-- Suggested labels: AI, DevOps, Operations, Incident Response, Documentation, CIR --&gt;
&lt;!-- Suggested meta description: Why I now capture context, intent, and rationale before live investigations or fixes, and how that one habit prevents repeated mistakes later. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It is not a vendor tutorial. It is a working rule I adopted after too many live investigations started with a terminal and ended with fuzzy memory.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Context logging cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEimeLrd_op-fT4GoFtMmHN341Jo8LovukwomFFZaRQ6F_JKyxTcaabN8iUw_0qz3yMS-BdSutsJn15cyzHGI7TaJBXfswdVvkLIaJfcGAP-p1kUsKZiCUuhSiMQGVrqL_-hqrnTB_EVhAUBe2XFn9mP-umHSKsO7uPuu_4DlcOw4Big3xLemLwykoLq_FzG&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The mistake is easy to make:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;something looks wrong&lt;/li&gt;
&lt;li&gt;I jump into the shell&lt;/li&gt;
&lt;li&gt;I start reading logs, manifests, dashboards, or browser state&lt;/li&gt;
&lt;li&gt;two hours later I remember the fix, but not the full reasoning path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is exactly how the same class of incident keeps returning in slightly different forms.&lt;/p&gt;

&lt;h2&gt;What I write down before I start&lt;/h2&gt;

&lt;p&gt;I do not try to write a novel. I write three things:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;What I want&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Context&lt;/td&gt;&lt;td&gt;What is happening right now, and what changed recently&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Intent&lt;/td&gt;&lt;td&gt;What I am trying to prove or change&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Rationale&lt;/td&gt;&lt;td&gt;Why this path is safer than the alternatives&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That is the smallest version of a change log that still survives a bad week.&lt;/p&gt;

&lt;h2&gt;Why I do this even for read-only work&lt;/h2&gt;

&lt;p&gt;People often treat documentation as a mutation-only habit. I think that is backwards.&lt;/p&gt;

&lt;p&gt;Read-only investigations still shape later decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which graphs I trusted&lt;/li&gt;
&lt;li&gt;which logs were misleading&lt;/li&gt;
&lt;li&gt;which browser path was blocked by auth or anti-bot checks&lt;/li&gt;
&lt;li&gt;which saved artifact became the source of truth later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I do not log that context while the investigation is fresh, the next run starts from folklore.&lt;/p&gt;

&lt;h2&gt;The exact failure pattern I am trying to avoid&lt;/h2&gt;

&lt;p&gt;The failure is not &quot;I forgot everything.&quot; It is more subtle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I remember the conclusion&lt;/li&gt;
&lt;li&gt;I forget the rejected paths&lt;/li&gt;
&lt;li&gt;I forget the external constraint&lt;/li&gt;
&lt;li&gt;I forget which proof actually convinced me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is dangerous because a future me can look at the final state and decide some &quot;unnecessary&quot; workaround should be removed, when in reality it was the part keeping the system safe.&lt;/p&gt;

&lt;h2&gt;My practical template&lt;/h2&gt;

&lt;p&gt;This is the kind of note I want near the work:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Context: public traffic looked down on one origin, but the service was partially balanced elsewhere.
Intent: verify whether this is a real global drop or only a per-origin distribution change.
Rationale: checking the summed public path first avoids &quot;fixing&quot; a healthy balance event as if it were an outage.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That template works for code, dashboards, browser automation, one-off reports, and production runbooks.&lt;/p&gt;

&lt;h2&gt;A small diagram of the decision path&lt;/h2&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Context logging flow diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEh3vVZzsOStTd0nHnLGj_w__6rv2J4s9RnEIw7jKbip38_ZX18dKIEVkCKsktMC1FFyVBcysiEGD3LS-PzoxN4DAkyfc_nmcDWZGuKWXR1euUqCHhkh5yNcP3QWIHv6Reld-mm9QhURxjrihPOKlTd8JV_igv7wYtXPOYq4GcqklBFnxLsZ1FekzgidJPDD&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The point of that flow is simple: I want the note *before* I accumulate terminal tabs, ad hoc commands, and screenshots I will not be able to explain later.&lt;/p&gt;

&lt;h2&gt;The command trail I usually keep&lt;/h2&gt;

&lt;p&gt;I do not dump every command. I keep the commands that define the reasoning path:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# 1. Confirm the public symptom
curl -I https://example-service.invalid/health

# 2. Check whether the issue is global or isolated
kubectl get pods -n example-namespace -o wide

# 3. Preserve the before-state artifact
kubectl get deployment example -n example-namespace -o yaml &amp;gt; before.yaml

# 4. Check the data source behind the alert
python3 inspect-metric-source.py --metric service_request_rate&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is enough to reconstruct the investigation later without pretending the shell history is documentation.&lt;/p&gt;

&lt;h2&gt;Failure case: when I skip the note&lt;/h2&gt;

&lt;p&gt;When I skip the note, the same bad pattern appears:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the artifact folder contains files but not their meaning&lt;/li&gt;
&lt;li&gt;the dashboard screenshot exists but not why it mattered&lt;/li&gt;
&lt;li&gt;the final fix is remembered as &quot;obvious&quot;&lt;/li&gt;
&lt;li&gt;the next operator repeats the discarded approach first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how harmless-looking cleanup becomes accidental regression.&lt;/p&gt;

&lt;h2&gt;What this changes when AI agents are involved&lt;/h2&gt;

&lt;p&gt;AI agents amplify the value of explicit context because they can move faster than humans through the same investigation surface.&lt;/p&gt;

&lt;p&gt;That is useful only if the task is bounded clearly enough that the agent is not inventing the contract while it works.&lt;/p&gt;

&lt;p&gt;Without a context note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent may optimize the wrong thing&lt;/li&gt;
&lt;li&gt;a saved artifact may be treated as authoritative when it was only a probe&lt;/li&gt;
&lt;li&gt;a fallback path may be removed because it looks redundant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a context note, the agent is executing inside a known frame.&lt;/p&gt;

&lt;h2&gt;A simple operator checklist&lt;/h2&gt;

&lt;p&gt;Before touching a live-adjacent system, I now want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one sentence for the symptom&lt;/li&gt;
&lt;li&gt;one sentence for the intended outcome&lt;/li&gt;
&lt;li&gt;one sentence for why this path is the least risky&lt;/li&gt;
&lt;li&gt;one artifact that captures the before-state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a very low bar, but it removes a surprising amount of confusion.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;I used to think I needed &quot;full documentation&quot; before any of this would help. I no longer believe that.&lt;/p&gt;

&lt;p&gt;What I would do differently now is start with a tiny Context / Intent / Rationale note first, then let the richer artifacts accumulate around it. Small, explicit notes beat larger undocumented piles of output every time.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1073217737204446139'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1073217737204446139'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/why-i-log-context-before-i-touch.html' title='Why I Log Context Before I Touch Anything Live'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEimeLrd_op-fT4GoFtMmHN341Jo8LovukwomFFZaRQ6F_JKyxTcaabN8iUw_0qz3yMS-BdSutsJn15cyzHGI7TaJBXfswdVvkLIaJfcGAP-p1kUsKZiCUuhSiMQGVrqL_-hqrnTB_EVhAUBe2XFn9mP-umHSKsO7uPuu_4DlcOw4Big3xLemLwykoLq_FzG=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-4878265838981283536</id><published>2026-05-05T09:53:00.003+02:00</published><updated>2026-05-05T09:53:00.120+02:00</updated><title type='text'>The Hidden Maintenance Cost of a Small Kubernetes Cluster</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: The Hidden Maintenance Cost of a Small Kubernetes Cluster --&gt;
&lt;!-- Suggested labels: Kubernetes, DevOps, Cost, Operations, Monitoring, Hetzner, Series --&gt;
&lt;!-- Suggested meta description: The real maintenance costs of running a small Kubernetes cluster are not only on the invoice. This post covers the hidden operational work that accumulates around images, monitoring, storage, and recovery. --&gt;


&lt;p&gt;This article is part 15 of my MisLinux series on Kubernetes on Hetzner. It reflects my own operating experience and cost tradeoffs, not a vendor benchmark. I am not affiliated with Hetzner or any other vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Hidden maintenance cost cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiHJ4ZGsQsNCbpHMy3_bPNXhTiARlFcRWB-6I0OBpiBQwV_1A7tA5EO223zUHsGCvCnqi9IDknZBBCHzv6peDl1-J0P0cN7VSwvB5AdVztnIINlN4EROTTZaFKYJUuTa2F62pLrJF4F5S5aFEzw653O2p1PuHJRn2QKTCFgS78lfeaqeEzEn4GdV59C7EzN&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When people compare the cost of a small Kubernetes cluster against a managed-cloud alternative, they usually compare invoices first. That is sensible, but incomplete.&lt;/p&gt;

&lt;p&gt;The invoice matters. It just is not the whole bill.&lt;/p&gt;

&lt;p&gt;There is a second cost layer that shows up in operator time, debugging attention, and the number of low-level details you have to keep honest as the cluster ages. That is the hidden maintenance cost, and it is one of the main reasons a small cluster can be either a smart decision or a bad bargain depending on who is running it.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Hidden maintenance cost category chart&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiuwWH69Ly9RLkJFRTsl8-jwxX2WjKgwI1RBbwkTrW3pgu_qCnHg56VePczPxRYqz9H2weZhIO8BmJBGoonXRd9pXxPC1OhjcDGC2RhUfDadmiigTyW5ciqG0E6hiwjYGH5HLOxoyNIiRf4CM0LzmSO4ISJFBaktFwlnAs7eCiKU2DORnGuDGw5uqCzVBhI&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;The hidden cost is not &quot;Kubernetes is hard&quot;&lt;/h2&gt;

&lt;p&gt;I do not find the phrase &quot;Kubernetes is hard&quot; especially useful. The more practical truth is:&lt;/p&gt;

&lt;p&gt;&amp;gt; Kubernetes is a multiplier on the quality of your operational habits.&lt;/p&gt;

&lt;p&gt;If your cluster is small and focused, the visible infrastructure bill can be very attractive. What grows in parallel is the maintenance surface around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image publishing discipline&lt;/li&gt;
&lt;li&gt;registry storage hygiene&lt;/li&gt;
&lt;li&gt;node disk pressure and log growth&lt;/li&gt;
&lt;li&gt;monitoring retention tuning&lt;/li&gt;
&lt;li&gt;backup and restore confidence&lt;/li&gt;
&lt;li&gt;failover preparation&lt;/li&gt;
&lt;li&gt;architecture-specific build handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are shocking individually. Together, they become a real recurring cost center.&lt;/p&gt;

&lt;h2&gt;What the invoice misses&lt;/h2&gt;

&lt;p&gt;On the invoice, I can usually see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compute&lt;/li&gt;
&lt;li&gt;volumes&lt;/li&gt;
&lt;li&gt;IPv4 addresses&lt;/li&gt;
&lt;li&gt;backups&lt;/li&gt;
&lt;li&gt;bandwidth when relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the invoice does not show is the human maintenance needed to keep the cluster from drifting into fragility.&lt;/p&gt;

&lt;p&gt;That hidden work often includes:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Hidden cost area&lt;/th&gt;&lt;th&gt;What it really means&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Registry maintenance&lt;/td&gt;&lt;td&gt;not just storing images, but protecting pullability, multi-arch correctness, and safe cleanup&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Monitoring maintenance&lt;/td&gt;&lt;td&gt;keeping alerts meaningful, storage right-sized, and exporter scope honest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Node hygiene&lt;/td&gt;&lt;td&gt;log routing, image GC, ephemeral storage checks, and &quot;why is this disk filling?&quot; investigations&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Recovery readiness&lt;/td&gt;&lt;td&gt;validating snapshots, standby paths, credentials, and DNS cutover steps before a crisis&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Automation safety&lt;/td&gt;&lt;td&gt;making sure the things that save time do not create silent production risk&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This is why a cheap cluster can still be expensive if the operating discipline is weak.&lt;/p&gt;

&lt;h2&gt;Why small clusters are especially tricky&lt;/h2&gt;

&lt;p&gt;A large organization often spreads these responsibilities across roles. A small cluster usually does not get that luxury.&lt;/p&gt;

&lt;p&gt;The same person or small team ends up owning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;platform design&lt;/li&gt;
&lt;li&gt;incident response&lt;/li&gt;
&lt;li&gt;registry maintenance&lt;/li&gt;
&lt;li&gt;monitoring hygiene&lt;/li&gt;
&lt;li&gt;build troubleshooting&lt;/li&gt;
&lt;li&gt;application rollout validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That concentration can be efficient. It can also mean that every unresolved low-level maintenance issue becomes future debt for the same operator.&lt;/p&gt;

&lt;h2&gt;The maintenance that surprised me most&lt;/h2&gt;

&lt;p&gt;The areas that changed my thinking the most were not the ones that felt dramatic initially.&lt;/p&gt;

&lt;h3&gt;Registry correctness&lt;/h3&gt;

&lt;p&gt;I expected the registry to need disk space and backup attention. What surprised me was how much care is needed around tag semantics, cleanup heuristics, and multi-architecture manifest safety. That is not just storage administration. It is deployment correctness.&lt;/p&gt;

&lt;h3&gt;Monitoring retention&lt;/h3&gt;

&lt;p&gt;I expected Prometheus and Grafana to need resources. What surprised me was how quickly retention strategy becomes a design problem rather than a configuration detail once you care about restart behavior, WAL replay, and realistic history windows.&lt;/p&gt;

&lt;h3&gt;Node disk pressure&lt;/h3&gt;

&lt;p&gt;I expected workloads to use disk. What surprised me was how much slow, indirect growth comes from side effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unrotated logs&lt;/li&gt;
&lt;li&gt;stale image layers&lt;/li&gt;
&lt;li&gt;abandoned temp files&lt;/li&gt;
&lt;li&gt;orphaned overlay snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the kinds of costs that do not show up in architectural diagrams but absolutely show up in late-night debugging.&lt;/p&gt;

&lt;h2&gt;Why this does not invalidate the small-cluster choice&lt;/h2&gt;

&lt;p&gt;The hidden maintenance cost is real, but it does not automatically mean &quot;do not run the cluster yourself.&quot;&lt;/p&gt;

&lt;p&gt;It means you need to compare two honest totals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;vendor invoice plus managed-service convenience&lt;/li&gt;
&lt;li&gt;lower infrastructure invoice plus self-managed maintenance burden&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For some workloads, the second option is still clearly better. That has often been my experience. But only if the operator is willing to count the maintenance burden as part of the decision instead of pretending the invoice tells the whole story.&lt;/p&gt;

&lt;h2&gt;The question I ask now before adding anything&lt;/h2&gt;

&lt;p&gt;Whenever I add infrastructure to a small cluster, I ask:&lt;/p&gt;

&lt;p&gt;&amp;gt; What new maintenance loop does this create?&lt;/p&gt;

&lt;p&gt;Not &quot;can I deploy it?&quot; Not even &quot;is it useful?&quot;&lt;/p&gt;

&lt;p&gt;The most important question is whether it creates a new recurring responsibility around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cleanup&lt;/li&gt;
&lt;li&gt;rotation&lt;/li&gt;
&lt;li&gt;recovery&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;correctness validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That question helps me avoid systems that look cheap to start and expensive to keep honest.&lt;/p&gt;

&lt;h2&gt;What I think small-cluster operators should optimize for&lt;/h2&gt;

&lt;p&gt;I do not think the right goal is &quot;run as much as possible yourself.&quot; I think the right goal is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the architecture understandable&lt;/li&gt;
&lt;li&gt;automate the repeatable pain&lt;/li&gt;
&lt;li&gt;document the non-obvious failure modes&lt;/li&gt;
&lt;li&gt;avoid adding components whose maintenance cost is larger than the problem they solve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what makes a small Kubernetes cluster sustainable rather than merely affordable.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;The hidden maintenance cost of a small Kubernetes cluster is not a reason to dismiss the model. It is a reason to evaluate it honestly.&lt;/p&gt;

&lt;p&gt;If you only compare the provider invoice, you will underestimate the real price. If you only compare the maintenance burden, you will miss how much control and cost-efficiency a small cluster can still offer.&lt;/p&gt;

&lt;p&gt;The useful middle ground is to treat maintenance time as part of the platform budget. Once I started doing that, my infrastructure decisions got less romantic and more reliable.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4878265838981283536'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/4878265838981283536'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/the-hidden-maintenance-cost-of-small.html' title='The Hidden Maintenance Cost of a Small Kubernetes Cluster'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEiHJ4ZGsQsNCbpHMy3_bPNXhTiARlFcRWB-6I0OBpiBQwV_1A7tA5EO223zUHsGCvCnqi9IDknZBBCHzv6peDl1-J0P0cN7VSwvB5AdVztnIINlN4EROTTZaFKYJUuTa2F62pLrJF4F5S5aFEzw653O2p1PuHJRn2QKTCFgS78lfeaqeEzEn4GdV59C7EzN=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-2042457193278368634</id><published>2026-05-01T17:41:00.005+02:00</published><updated>2026-05-05T16:47:16.323+02:00</updated><title type='text'>Why &#39;Latest&#39; Is Not a Deployment Strategy in a Self-Hosted Registry</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why &#39;Latest&#39; Is Not a Deployment Strategy in a Self-Hosted Registry --&gt;
&lt;!-- Suggested labels: Docker, Kubernetes, Registry, DevOps, Deployment, Series --&gt;
&lt;!-- Suggested meta description: Why relying on latest is operationally weak in a self-hosted registry, what it hides, and the safer tagging and promotion habits I use instead. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 14 of my MisLinux series on Kubernetes on Hetzner. It reflects my own production experience with a self-hosted registry. I am not affiliated with Docker or any vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Latest tag deployment cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjFrJRjaRFn44EOSu_NcUuRCkbMSStI8DWbGY2VEF4gyKx_5R0XZLzVXFbghZ0dERMrzG50sIUQ9vCZJG6Z1gjMHLSctMhP5h57L-zh8MVvqvacQFsoKbM8UuOsE17GlebIt4ArDT0_901dOkfK6M8PrySWjsUdwUy4vPV7tNEtyZCPNc5Z4MEy-a8L03CC&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;There is a version of &lt;code&gt;latest&lt;/code&gt; that is harmless: the local development convenience tag you use while building or testing. That is not the version I am talking about.&lt;/p&gt;

&lt;p&gt;The version I mean is &lt;code&gt;latest&lt;/code&gt; as an operational contract in a real self-hosted registry, where storage pressure, cleanup policy, multi-architecture manifests, cached images, and historical provenance all matter.&lt;/p&gt;

&lt;p&gt;In that world, &lt;code&gt;latest&lt;/code&gt; is not a deployment strategy. It is a label sitting on top of a set of assumptions. If those assumptions are weak, the label becomes misleading very quickly.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Tag promotion and provenance chart&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiupBYGXBvjL3bsIrkavVsc9Gvqhp1j5cqX2LawqUNSlgXZisvaKuJKmv8dGQ9YospculFx_NUxhit6WfvYQHzpJjVe8sX2R2CblubSNYsq0d1jnr0_by7QsHW_5rD5qFBDWujHwDs6o93204aaRKd4173N70X-LDcH9mfQFiYoM8FTKXiHGOcl_l0bUYrk&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;What &lt;code&gt;latest&lt;/code&gt; hides&lt;/h2&gt;

&lt;p&gt;The problem with &lt;code&gt;latest&lt;/code&gt; is not that it points to a recent image. The problem is that it hides the answer to the question operators actually need:&lt;/p&gt;

&lt;p&gt;&amp;gt; Which exact image, built from which exact inputs, is this workload supposed to be running?&lt;/p&gt;

&lt;p&gt;That matters even more in a self-hosted registry because the registry itself is part of your operational surface. A tag is not an abstract truth. It is a mutable pointer living inside a system you also maintain.&lt;/p&gt;

&lt;p&gt;When I depend too heavily on &lt;code&gt;latest&lt;/code&gt;, I lose clarity around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provenance&lt;/li&gt;
&lt;li&gt;promotion order&lt;/li&gt;
&lt;li&gt;rollback path&lt;/li&gt;
&lt;li&gt;multi-arch descriptor protection&lt;/li&gt;
&lt;li&gt;what is actually safe to clean up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Why self-hosting makes this worse&lt;/h2&gt;

&lt;p&gt;Managed registries already make tag discipline important. Self-hosting raises the stakes because the registry can have its own incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storage pressure&lt;/li&gt;
&lt;li&gt;cleanup mistakes&lt;/li&gt;
&lt;li&gt;missing child manifests under multi-arch tags&lt;/li&gt;
&lt;li&gt;cached-vs-pullable mismatches&lt;/li&gt;
&lt;li&gt;broken rebuilds that do not become visible until the next node restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all of that sits behind a single mutable &lt;code&gt;latest&lt;/code&gt;, the operator story gets weak fast.&lt;/p&gt;

&lt;h2&gt;What I use &lt;code&gt;latest&lt;/code&gt; for now&lt;/h2&gt;

&lt;p&gt;I still use &lt;code&gt;latest&lt;/code&gt;, but I treat it as a convenience pointer, not as the authoritative identity of the image.&lt;/p&gt;

&lt;p&gt;The authoritative identity is one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an immutable commit-derived tag&lt;/li&gt;
&lt;li&gt;a specific digest&lt;/li&gt;
&lt;li&gt;a clearly named staged/runtime promotion tag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;latest&lt;/code&gt; can exist on top of that, but it should not be the only thing you know.&lt;/p&gt;

&lt;h2&gt;Why immutable tags matter more than pretty tags&lt;/h2&gt;

&lt;p&gt;In my environment, some build tags are ugly. They encode the source commits that went into the image. Operationally, that ugliness is a feature.&lt;/p&gt;

&lt;p&gt;An immutable provenance tag answers questions &lt;code&gt;latest&lt;/code&gt; cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what source state produced this image?&lt;/li&gt;
&lt;li&gt;is this the same thing I built yesterday or just something also called &lt;code&gt;latest&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;can I rebuild or compare it safely?&lt;/li&gt;
&lt;li&gt;is this the tag I should preserve because a live workload still depends on its digest?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pretty tags are easier to read. Immutable tags are easier to trust.&lt;/p&gt;

&lt;h2&gt;The promotion model I find safer&lt;/h2&gt;

&lt;p&gt;I prefer a layered tagging model:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Tag type&lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Immutable build tag&lt;/td&gt;&lt;td&gt;Exact provenance of one build&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Stage/runtime tag&lt;/td&gt;&lt;td&gt;Operational promotion step with a concrete meaning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Convenience alias, never the only truth&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That structure lets me separate build identity from human-friendly promotion state.&lt;/p&gt;

&lt;p&gt;It also makes rollback better. If a runtime tag is bad, I can see what it used to point to. If &lt;code&gt;latest&lt;/code&gt; is the only visible identity, rollback becomes a memory exercise.&lt;/p&gt;

&lt;h2&gt;Why this matters even when Kubernetes pins digests&lt;/h2&gt;

&lt;p&gt;It is fair to say: if workloads are pinned by digest, why care about &lt;code&gt;latest&lt;/code&gt; at all?&lt;/p&gt;

&lt;p&gt;Because the operator still has to do all of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect what new builds produced&lt;/li&gt;
&lt;li&gt;compare live digests to current tags&lt;/li&gt;
&lt;li&gt;rebuild missing pull paths&lt;/li&gt;
&lt;li&gt;clean up old manifests safely&lt;/li&gt;
&lt;li&gt;explain to another human what &quot;the right image&quot; means&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Digest pinning helps. It does not replace a sane promotion vocabulary.&lt;/p&gt;

&lt;h2&gt;The cleanup angle most people miss&lt;/h2&gt;

&lt;p&gt;The registry cleanup story is another reason &lt;code&gt;latest&lt;/code&gt; is not enough.&lt;/p&gt;

&lt;p&gt;If a cleanup policy says &quot;keep &lt;code&gt;latest&lt;/code&gt; and the newest N tags,&quot; that sounds reasonable until you realize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;latest&lt;/code&gt; may point to a multi-arch manifest list&lt;/li&gt;
&lt;li&gt;child manifests may still need direct protection&lt;/li&gt;
&lt;li&gt;live workloads may still be running older digests&lt;/li&gt;
&lt;li&gt;the immutable provenance tag may be the only safe direct reference left&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I prefer cleanup logic based on reachability and deployment usage instead of only on visible tag names.&lt;/p&gt;

&lt;h2&gt;What changed in how I deploy&lt;/h2&gt;

&lt;p&gt;I think of image deployment in three questions now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what exact immutable build did I produce?&lt;/li&gt;
&lt;li&gt;what exact build did I promote to the intended runtime tag?&lt;/li&gt;
&lt;li&gt;what exact digest is the workload actually using?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I cannot answer all three, then I am not really operating the image path deliberately. I am mostly trusting conventions to hold.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;latest&lt;/code&gt; is convenient, and convenience has its place. But convenience is not the same thing as a deployment strategy.&lt;/p&gt;

&lt;p&gt;A deployment strategy needs provenance, promotion, rollback, and cleanup safety. In a self-hosted registry, those requirements are too important to hide behind one mutable tag and a hopeful mental model.&lt;/p&gt;

&lt;p&gt;That is why I still keep &lt;code&gt;latest&lt;/code&gt; around, but I no longer let it pretend to be more than it is.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2042457193278368634'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2042457193278368634'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/05/why-latest-is-not-deployment-strategy.html' title='Why &#39;Latest&#39; Is Not a Deployment Strategy in a Self-Hosted Registry'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjFrJRjaRFn44EOSu_NcUuRCkbMSStI8DWbGY2VEF4gyKx_5R0XZLzVXFbghZ0dERMrzG50sIUQ9vCZJG6Z1gjMHLSctMhP5h57L-zh8MVvqvacQFsoKbM8UuOsE17GlebIt4ArDT0_901dOkfK6M8PrySWjsUdwUy4vPV7tNEtyZCPNc5Z4MEy-a8L03CC=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-1336895246921556303</id><published>2026-04-29T12:41:00.004+02:00</published><updated>2026-05-05T16:47:04.555+02:00</updated><title type='text'>My Cold-Standby Disaster Recovery Plan for a Single Critical Service</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: My Cold-Standby Disaster Recovery Plan for a Single Critical Service --&gt;
&lt;!-- Suggested labels: Disaster Recovery, Kubernetes, Failover, Backup, DevOps, Hetzner, Series --&gt;
&lt;!-- Suggested meta description: How I designed a cold-standby recovery plan for one critical service, why I chose snapshots and manual DNS cutover, and what I consider acceptable complexity for a small platform. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 13 of my MisLinux series on Kubernetes on Hetzner. It is based on a real production service and the recovery planning around it. I am not affiliated with Hetzner, Cloudflare, Netcup, or any vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Cold standby disaster recovery cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjuk-ePiRHOzE9W3cdjLKaT-RIxNCs_2ljb_CD0LSpXFfsgU-El7aAnlX7NcsDLI2-T-X1Qm_-qs7E-LY3rKLImLNwgA0F1U94ANSZmJDyQ01BgRE7FQhLv8CYvP2rynqOMNJqCbvzCP5jm8cAc4Y6eMxJ7OamyHCnxfnyHO_Ye8oom9-3Sd_v5WfqmYqOu&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;If you run a small platform, &quot;disaster recovery&quot; can become a vague comfort word very quickly. It is easy to say you can rebuild from source, recreate the cluster, restore the database, and recover the service. It is much harder to define what you will actually do if the main environment is gone or unreachable and a critical public endpoint needs to come back fast.&lt;/p&gt;

&lt;p&gt;That difference is why I ended up designing a very explicit cold-standby plan for one service instead of pretending the whole platform needed full hot-failover sophistication.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Cold standby failover sequence diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjj3-UwQ7zlisky--vCisAC_-2DRH2hXLLFBH3c1wLva8CbpYd_GHI8_SzskrjC_EUakaHOrcbExJO1m6YkDTDz3ysYhuItE3OrhQA9VBkrGrbVweLTwZwnO55gymDMq5kIDDmdSauK_NSUKH6zrX6ND5ygoknl6zFc1feUwMpaRsVgjqDBglRGMmW2cx-Y&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;Why I focused on one service first&lt;/h2&gt;

&lt;p&gt;Not every service deserves the same recovery model.&lt;/p&gt;

&lt;p&gt;Some services are annoying to lose. Some are expensive to lose. A smaller number are central enough that their absence immediately becomes a business problem.&lt;/p&gt;

&lt;p&gt;For me, the right first DR target was a single critical serving path. That let me answer the recovery question honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what exact artifact does this service need to start?&lt;/li&gt;
&lt;li&gt;where can I store that artifact outside the cluster?&lt;/li&gt;
&lt;li&gt;how would I run the same runtime elsewhere if Hetzner were unavailable?&lt;/li&gt;
&lt;li&gt;what manual step am I willing to take for cutover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a much healthier question than &quot;how do I make the whole cluster magically highly available?&quot;&lt;/p&gt;

&lt;h2&gt;Why I chose cold standby instead of active-active&lt;/h2&gt;

&lt;p&gt;I did not want to build a complicated active-active or automatic failover design for a small environment unless the business case clearly justified it.&lt;/p&gt;

&lt;p&gt;The cost of more advanced failover is not just extra infrastructure. It is operational ambiguity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which side is authoritative?&lt;/li&gt;
&lt;li&gt;how do you keep config perfectly in sync?&lt;/li&gt;
&lt;li&gt;what happens during split-brain conditions?&lt;/li&gt;
&lt;li&gt;how do you test the failover path without risking production?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For one critical service, a cold standby with good artifacts and a documented cutover path was a better fit.&lt;/p&gt;

&lt;p&gt;My design goal became:&lt;/p&gt;

&lt;p&gt;&amp;gt; keep one externally runnable copy of the service ready enough that I can start it from a known-good snapshot and then cut DNS over manually if the primary environment is gone.&lt;/p&gt;

&lt;h2&gt;What the standby actually needs&lt;/h2&gt;

&lt;p&gt;Once I stripped away the vague language, the required ingredients were simpler than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the exact runtime image or equivalent runnable artifact&lt;/li&gt;
&lt;li&gt;the last known-good serving configuration&lt;/li&gt;
&lt;li&gt;a minimal host or VM where that runtime can start&lt;/li&gt;
&lt;li&gt;a TLS front door&lt;/li&gt;
&lt;li&gt;a manual DNS cutover path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the core of the plan. Everything else is support structure.&lt;/p&gt;

&lt;h2&gt;Why the config snapshot matters so much&lt;/h2&gt;

&lt;p&gt;The most important insight was that disaster recovery for a service often depends more on the last known-good runtime &lt;strong&gt;state&lt;/strong&gt; than on the source code.&lt;/p&gt;

&lt;p&gt;Source code is necessary, but it is not the immediate recovery substrate in a crisis. If the live service depends on a generated configuration that reflects database-backed objects, rules, or mappings, then the practical recovery question is:&lt;/p&gt;

&lt;p&gt;&amp;gt; Do I have the actual config the service was serving from?&lt;/p&gt;

&lt;p&gt;If the answer is no, then &quot;we can rebuild it from the database&quot; only works if the whole control plane and upstream data path are still available during the incident. That is not a disaster assumption I want to build around.&lt;/p&gt;

&lt;p&gt;So I prefer taking validated off-cluster snapshots of the real serving config instead.&lt;/p&gt;

&lt;h2&gt;Why the cutover stays manual&lt;/h2&gt;

&lt;p&gt;A small platform can talk itself into dangerous automation very easily.&lt;/p&gt;

&lt;p&gt;Automatic failover sounds mature, but it only helps if the automation is more reliable than the operator judgment it replaces. For my environment, I do not think that is true yet.&lt;/p&gt;

&lt;p&gt;That is why DNS cutover stays manual.&lt;/p&gt;

&lt;p&gt;Manual cutover buys me two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I decide based on evidence, not on one possibly misleading health signal&lt;/li&gt;
&lt;li&gt;I avoid accidental flapping between origins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, that means a documented runbook is more valuable than a half-trusted automatic failover controller.&lt;/p&gt;

&lt;h2&gt;What the standby is meant to prove&lt;/h2&gt;

&lt;p&gt;The cold standby is not meant to be a second production platform in permanent parallel service. It is meant to prove four things:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Proof goal&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;The service can start elsewhere from the saved config&lt;/td&gt;&lt;td&gt;Prevents DR from depending on cluster-local state or wishful re-generation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;The runtime image is available outside the primary pull path&lt;/td&gt;&lt;td&gt;Avoids turning registry or network dependency into the next blocker&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TLS and reverse proxying work on the standby host&lt;/td&gt;&lt;td&gt;Makes cutover realistic instead of theoretical&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DNS can be pointed at the standby origin intentionally&lt;/td&gt;&lt;td&gt;Turns the recovery path into an operator action, not a design diagram&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That is enough for a credible first DR layer on a small platform.&lt;/p&gt;

&lt;h2&gt;What I do not want from DR&lt;/h2&gt;

&lt;p&gt;I do not want the disaster-recovery system to become a second fragile platform with its own hidden dependencies.&lt;/p&gt;

&lt;p&gt;That means I try to avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rebuilding from source during the incident&lt;/li&gt;
&lt;li&gt;assuming private registries are reachable&lt;/li&gt;
&lt;li&gt;depending on interactive browser-only recovery steps&lt;/li&gt;
&lt;li&gt;distributing the failover state across too many places&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more moving parts the DR path has, the less likely it is to be trustworthy under actual pressure.&lt;/p&gt;

&lt;h2&gt;The tradeoff I accept&lt;/h2&gt;

&lt;p&gt;Cold standby is not instant recovery. I accept that.&lt;/p&gt;

&lt;p&gt;The real tradeoff is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;slower recovery&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;in exchange for&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a simpler, more explicit, more testable path&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small team, that is often the right bargain. I would rather have a cold standby that I can explain and rehearse than an elaborate failover story I never fully trust.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;The most useful change in how I think about DR is this: disaster recovery starts becoming real only when you can name the exact artifact, the exact host, and the exact operator action that would bring the service back.&lt;/p&gt;

&lt;p&gt;Until then, it is mostly optimism dressed up as architecture.&lt;/p&gt;

&lt;p&gt;For me, the right first step was not platform-wide high availability. It was proving that one critical service could start elsewhere from a real snapshot and be put back behind its public hostname deliberately. That is not the final state. But it is a recovery plan I actually believe.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1336895246921556303'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1336895246921556303'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/my-cold-standby-disaster-recovery-plan.html' title='My Cold-Standby Disaster Recovery Plan for a Single Critical Service'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjuk-ePiRHOzE9W3cdjLKaT-RIxNCs_2ljb_CD0LSpXFfsgU-El7aAnlX7NcsDLI2-T-X1Qm_-qs7E-LY3rKLImLNwgA0F1U94ANSZmJDyQ01BgRE7FQhLv8CYvP2rynqOMNJqCbvzCP5jm8cAc4Y6eMxJ7OamyHCnxfnyHO_Ye8oom9-3Sd_v5WfqmYqOu=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-6163047782274531910</id><published>2026-04-27T09:41:00.004+02:00</published><updated>2026-05-05T16:46:53.156+02:00</updated><title type='text'>How I Built an External Watchdog So My Cluster Is Monitored From the Outside Too</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Built an External Watchdog So My Cluster Is Monitored From the Outside Too --&gt;
&lt;!-- Suggested labels: Kubernetes, Monitoring, Watchdog, Grafana, Prometheus, DevOps, Series --&gt;
&lt;!-- Suggested meta description: Why in-cluster monitoring is not enough by itself, and how I added an external watchdog that checks my services from outside the cluster. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 12 of my MisLinux series on Kubernetes on Hetzner. It is based on a real operational setup I use. I am not affiliated with Synology, Grafana Labs, Hetzner, or any vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;External watchdog cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEj5iyJmjAblEXjLs4DeqNK4s0D9swuIwrzKvu7Fm35YU0JG-Excv2C54b0xxErr1Svt9Wq-V0kQSeYPiQGlglNj6zzfsa_LyESV1OaWNks5LvPlYUaoP3awby-RWHdXaUAZ6pYQv5KS_BDPi4RMR5sVj0OrfAaeihByKn08ir0BgJuChA_JJXL0UegHGJln&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When people say &quot;the cluster is monitored,&quot; they often mean &quot;the cluster is monitoring itself.&quot; That is useful, but it is not the same thing.&lt;/p&gt;

&lt;p&gt;An in-cluster monitoring stack can tell you a great deal about node pressure, pod restarts, internal health endpoints, and application metrics. What it cannot prove by itself is that the cluster is still reachable from the outside world under the same conditions your users see.&lt;/p&gt;

&lt;p&gt;That gap matters more than it sounds.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;External watchdog architecture diagram&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiQTgQ6JpJDoq30qI1nm2hmvOinhrEB-HVCtrZLTyiaf63I31dPfb5mBa-jtrjzt2sXJpRT0hML2QGWCVCYlNZPsCFYnxnAwC6klpNzg0q92NqAFdhd2Bwe5LNpytq7PmzDINsnrdWE6OVzh1KHM_PV9CU-G_KxJSKX_dfZwhYmW-fIlGN9MmUgTqTaqvns&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;Why internal monitoring is not enough on its own&lt;/h2&gt;

&lt;p&gt;If all of your monitoring, alerting, and dashboards live inside the system being monitored, you inherit its blind spots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a networking break can isolate the cluster and the monitoring at the same time&lt;/li&gt;
&lt;li&gt;DNS or ingress issues can make public services fail while internal health checks still pass&lt;/li&gt;
&lt;li&gt;a bad change can break external reachability without breaking pod-level liveness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the cluster can be healthy in its own opinion while being unavailable in the way that actually matters.&lt;/p&gt;

&lt;p&gt;That is why I added a small external watchdog running outside the cluster.&lt;/p&gt;

&lt;h2&gt;What I wanted the watchdog to do&lt;/h2&gt;

&lt;p&gt;I did not want a second full observability stack. I wanted a narrow, opinionated external view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can the public URL resolve?&lt;/li&gt;
&lt;li&gt;does TLS still work?&lt;/li&gt;
&lt;li&gt;does the service answer quickly enough?&lt;/li&gt;
&lt;li&gt;if it fails, can I get a notification even if the cluster itself is confused?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a very different job from Prometheus inside the cluster. The external watchdog is not for rich diagnosis. It is for independent truth.&lt;/p&gt;

&lt;h2&gt;The design I ended up with&lt;/h2&gt;

&lt;p&gt;I run a lightweight watchdog process outside the cluster on a small always-on system. Its responsibilities are intentionally boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;poll the critical public endpoints&lt;/li&gt;
&lt;li&gt;enforce explicit connection and response timeouts&lt;/li&gt;
&lt;li&gt;send a Telegram message when a check fails&lt;/li&gt;
&lt;li&gt;emit a periodic heartbeat back toward the cluster so the cluster can notice if the watchdog itself disappears&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step matters. An external watchdog that silently dies is not much of a watchdog.&lt;/p&gt;

&lt;p&gt;So the design becomes two-way:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Direction&lt;/th&gt;&lt;th&gt;What it proves&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;External watchdog -&amp;gt; public services&lt;/td&gt;&lt;td&gt;The internet-facing path still works from outside the cluster&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cluster monitoring -&amp;gt; watchdog heartbeat&lt;/td&gt;&lt;td&gt;The watchdog itself is still alive and checking in&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Together, those signals are much more trustworthy than either one alone.&lt;/p&gt;

&lt;h2&gt;What the watchdog checks&lt;/h2&gt;

&lt;p&gt;I keep the checks simple and explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public service health endpoints or equivalent minimal URLs&lt;/li&gt;
&lt;li&gt;DNS resolution for important hostnames&lt;/li&gt;
&lt;li&gt;TLS validity and impending certificate expiry&lt;/li&gt;
&lt;li&gt;response-time budget, not just up/down state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is deliberately narrower than full application monitoring. If I want latency histograms or per-route error rates, Prometheus is the right place. If I want to know whether the service is still reachable from outside the cluster without trusting the cluster to tell me, the watchdog is the right place.&lt;/p&gt;

&lt;h2&gt;Why I like Telegram for this role&lt;/h2&gt;

&lt;p&gt;I wanted something easy to receive on a phone, easy to automate, and not dependent on setting up yet another mail or paging system. Telegram is a good fit for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;straightforward bot API&lt;/li&gt;
&lt;li&gt;fast delivery&lt;/li&gt;
&lt;li&gt;readable messages on mobile&lt;/li&gt;
&lt;li&gt;low friction for a small operational setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is not the specific chat platform. The important part is that the alert message explains itself clearly enough that I do not have to mentally reconstruct the system at the moment I receive it.&lt;/p&gt;

&lt;h2&gt;What makes the alerts useful instead of noisy&lt;/h2&gt;

&lt;p&gt;The external watchdog helped me reinforce a monitoring principle I already believed: alerts should explain what the failure probably means and what to check first.&lt;/p&gt;

&lt;p&gt;A useful alert says something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;which public service failed&lt;/li&gt;
&lt;li&gt;how it failed (timeout, DNS failure, bad status, TLS issue)&lt;/li&gt;
&lt;li&gt;what the first operator check should be&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is much better than a generic &quot;service down&quot; message, especially when the point of the watchdog is to be the first signal you receive.&lt;/p&gt;

&lt;h2&gt;Why I did not replace the in-cluster monitoring&lt;/h2&gt;

&lt;p&gt;The external watchdog is not a substitute for Prometheus, Grafana, node metrics, or per-service health instrumentation. It is a complement.&lt;/p&gt;

&lt;p&gt;Prometheus answers questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which node is filling up?&lt;/li&gt;
&lt;li&gt;which pod restarted?&lt;/li&gt;
&lt;li&gt;did the request rate collapse gradually or instantly?&lt;/li&gt;
&lt;li&gt;is this a localized workload issue or a broader cluster issue?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The watchdog answers a much simpler question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if I were outside the cluster right now, would the service look alive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That division of labor is exactly why the pairing works.&lt;/p&gt;

&lt;h2&gt;The real value during incidents&lt;/h2&gt;

&lt;p&gt;The biggest benefit is not prettier monitoring architecture. It is incident classification.&lt;/p&gt;

&lt;p&gt;When the external watchdog reports a failure and the internal cluster metrics are also sick, I know I am dealing with something broad.&lt;/p&gt;

&lt;p&gt;When the external watchdog reports a failure but the internal metrics still look normal, I immediately suspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS&lt;/li&gt;
&lt;li&gt;ingress&lt;/li&gt;
&lt;li&gt;certificate&lt;/li&gt;
&lt;li&gt;external routing&lt;/li&gt;
&lt;li&gt;origin reachability from outside the cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That narrows the response path quickly.&lt;/p&gt;

&lt;p&gt;Likewise, if the cluster starts alerting that the external watchdog heartbeat has stopped, I know the monitoring blind spot may have moved to the watchdog side rather than the service side.&lt;/p&gt;

&lt;h2&gt;What I would recommend to small-cluster operators&lt;/h2&gt;

&lt;p&gt;If you self-manage a small production cluster, my recommendation is not &quot;build a second observability platform.&quot; It is &quot;give yourself one independent external truth source.&quot;&lt;/p&gt;

&lt;p&gt;The minimum useful version can be very small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a handful of critical URL checks&lt;/li&gt;
&lt;li&gt;explicit timeouts&lt;/li&gt;
&lt;li&gt;one alert channel&lt;/li&gt;
&lt;li&gt;one heartbeat back into the cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough to cover a whole class of failures that purely internal monitoring does not see clearly.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;The reason I like an external watchdog is not that it is sophisticated. It is that it is independent.&lt;/p&gt;

&lt;p&gt;Operationally, independence is what gives the signal value. A cluster that says &quot;I am healthy&quot; is useful information. A cluster that is independently reachable from the outside is better information. When both views agree, I trust them more. When they disagree, I know where to investigate next.&lt;/p&gt;

&lt;p&gt;That is the real job of the watchdog: not to replace the internal monitoring stack, but to stop it from being the only voice in the room.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/6163047782274531910'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/6163047782274531910'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/how-i-built-external-watchdog-so-my.html' title='How I Built an External Watchdog So My Cluster Is Monitored From the Outside Too'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEj5iyJmjAblEXjLs4DeqNK4s0D9swuIwrzKvu7Fm35YU0JG-Excv2C54b0xxErr1Svt9Wq-V0kQSeYPiQGlglNj6zzfsa_LyESV1OaWNks5LvPlYUaoP3awby-RWHdXaUAZ6pYQv5KS_BDPi4RMR5sVj0OrfAaeihByKn08ir0BgJuChA_JJXL0UegHGJln=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-2775162202148449809</id><published>2026-04-24T11:17:00.004+02:00</published><updated>2026-05-05T16:46:40.674+02:00</updated><title type='text'>The Problem With hostPort DaemonSets in Production, and How I Test Them Safely</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: The Problem With hostPort DaemonSets in Production, and How I Test Them Safely --&gt;
&lt;!-- Suggested labels: Kubernetes, DaemonSet, hostPort, Operations, DevOps, Hetzner, Series --&gt;
&lt;!-- Suggested meta description: Why hostPort-based DaemonSets are operationally sharp in production, how a rollout can fail harder than a normal Deployment, and the safety pattern I use to test them. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 11 of my MisLinux series on Kubernetes on Hetzner. It reflects my own operational experience. I am not affiliated with Kubernetes, CNCF, or any vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;hostPort DaemonSet rollout cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjj2eU0jlgsggmVzRNwg8lzRNTIk2C5A87XPCspX9AkhKRJXq-nMbMlkeKBE-q6THKaWrIHoLaE0CD_jxJWMMQQxs9mYVhilJXioCHcEHUkdv-gfohECRHj4LsWF8LFS4hYtCUG_ZUT2YxilxSpxNOPBWYeCHHe9zrcNnW58rpkXcDPRI8tTgcp4IWAJloj&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;There are Kubernetes patterns that are perfectly reasonable until the day they have to be updated under pressure. A public-facing DaemonSet with &lt;code&gt;hostPort&lt;/code&gt; is one of them.&lt;/p&gt;

&lt;p&gt;I use this kind of pattern for a latency-sensitive service that I want reachable directly on each selected node. That is a valid design choice. The mistake would be pretending it rolls like an ordinary Deployment behind a Service. It does not.&lt;/p&gt;

&lt;h2&gt;Why &lt;code&gt;hostPort&lt;/code&gt; changes the rollout story&lt;/h2&gt;

&lt;p&gt;When a pod binds a &lt;code&gt;hostPort&lt;/code&gt;, it is competing for a real port on the node itself. That means a replacement pod cannot simply appear next to the old one if the old one is still holding the port.&lt;/p&gt;

&lt;p&gt;That single detail collapses a lot of the usual Kubernetes comfort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no meaningful same-node blue/green on the same port&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;maxSurge&lt;/code&gt; rescue if only one node serves that traffic path&lt;/li&gt;
&lt;li&gt;no assumption that a new pod can come up fully before the old one disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only have one node serving that workload class, a bad rollout is not a degraded rollout. It is an outage.&lt;/p&gt;

&lt;h2&gt;Why this is easy to underestimate&lt;/h2&gt;

&lt;p&gt;Kubernetes is so good at making ordinary workloads boring that it is easy to project the same confidence onto special cases.&lt;/p&gt;

&lt;p&gt;With a normal Deployment behind a Service, the rollout safety story is familiar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;new pods come up&lt;/li&gt;
&lt;li&gt;readiness gates traffic&lt;/li&gt;
&lt;li&gt;old pods drain away&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a &lt;code&gt;hostPort&lt;/code&gt; DaemonSet on one eligible node, the real story is closer to this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;old pod is removed or becomes unavailable&lt;/li&gt;
&lt;li&gt;new pod has to win the host port&lt;/li&gt;
&lt;li&gt;new pod still has to complete its own initialization&lt;/li&gt;
&lt;li&gt;if anything in that chain is wrong, public traffic drops immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a reason to avoid the pattern entirely. It is a reason to stop lying to yourself about the rollout risk.&lt;/p&gt;

&lt;h2&gt;The specific failure mode I care about most&lt;/h2&gt;

&lt;p&gt;The worst version of this pattern is not &quot;the pod crashes instantly.&quot; That is obvious. The more dangerous case is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the YAML looks correct&lt;/li&gt;
&lt;li&gt;the image tag looks correct&lt;/li&gt;
&lt;li&gt;the process starts&lt;/li&gt;
&lt;li&gt;but the service is not actually ready to answer the public request path yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can happen because of a slow startup sequence, a missing generated config, a probe that fires too early, or a runtime process that behaves differently after reload than after cold boot.&lt;/p&gt;

&lt;p&gt;In other words, the main risk is not syntax. It is false confidence.&lt;/p&gt;

&lt;h2&gt;The test pattern I trust now&lt;/h2&gt;

&lt;p&gt;I do not apply new behavior directly to the public DaemonSet anymore if I can avoid it. I first prove the runtime path in a parallel test DaemonSet that stays away from the live ports.&lt;/p&gt;

&lt;p&gt;The exact details depend on the workload, but the pattern is:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Step&lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Keep the public DaemonSet unchanged&lt;/td&gt;&lt;td&gt;Avoid accidental real traffic impact while validating the new image or command path&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Run a parallel test DaemonSet or equivalent pod on alternate ports&lt;/td&gt;&lt;td&gt;Prove startup, health checks, generated config, and runtime behavior without binding the production port&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Check the real readiness surface, not only container liveness&lt;/td&gt;&lt;td&gt;Make sure the service answers the same path the live system depends on&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Promote only after the parallel proof is clean&lt;/td&gt;&lt;td&gt;Treat the public rollout as the final step, not the experiment&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This is less elegant than a normal Deployment rollout, but it is honest about the shape of the risk.&lt;/p&gt;

&lt;h2&gt;What I specifically verify before promotion&lt;/h2&gt;

&lt;p&gt;My minimum checklist for a &lt;code&gt;hostPort&lt;/code&gt; DaemonSet change now includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image pulls correctly on the target architecture&lt;/li&gt;
&lt;li&gt;startup script finishes the full initialization path&lt;/li&gt;
&lt;li&gt;generated config files actually exist and are valid&lt;/li&gt;
&lt;li&gt;readiness only turns green after the real serving path works&lt;/li&gt;
&lt;li&gt;process supervision survives reloads, not only cold start&lt;/li&gt;
&lt;li&gt;the test instance answers a real request, not just a dummy socket check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I cannot prove those on the parallel test path, the production rollout is not ready.&lt;/p&gt;

&lt;h2&gt;Why readiness probes matter even more here&lt;/h2&gt;

&lt;p&gt;I used to think of readiness primarily as a traffic-quality control. In this pattern, it is also a self-defense mechanism against human optimism.&lt;/p&gt;

&lt;p&gt;If a service takes 90 to 180 seconds to become truly ready because it generates config from a database and only then launches the serving process, a 15-second readiness probe is not &quot;aggressive.&quot; It is wrong.&lt;/p&gt;

&lt;p&gt;That kind of mismatch is exactly how a Kubernetes manifest can look disciplined while still causing a live outage on rollout.&lt;/p&gt;

&lt;p&gt;The broader point is that readiness has to model the real application timeline, not the timeline you wish the application had.&lt;/p&gt;

&lt;h2&gt;When I still think this pattern is worth using&lt;/h2&gt;

&lt;p&gt;Even with the rollout risk, I still think &lt;code&gt;hostPort&lt;/code&gt; DaemonSets can be the right answer when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each selected node is meant to serve traffic directly&lt;/li&gt;
&lt;li&gt;you want to avoid an extra hop for the workload&lt;/li&gt;
&lt;li&gt;node-local identity actually matters&lt;/li&gt;
&lt;li&gt;the service is operationally narrow and worth the explicit handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the price of that pattern is operational ceremony. If you choose it, you are choosing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stricter promotion discipline&lt;/li&gt;
&lt;li&gt;stronger rollback expectations&lt;/li&gt;
&lt;li&gt;more careful probe tuning&lt;/li&gt;
&lt;li&gt;explicit non-production proof before live mutation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the actual cost, and it should be counted up front.&lt;/p&gt;

&lt;h2&gt;What I would not do again&lt;/h2&gt;

&lt;p&gt;I would not treat &quot;the manifest change looks small&quot; as a reason to skip the test path.&lt;/p&gt;

&lt;p&gt;Small changes can break this kind of workload in large ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a slightly different startup command&lt;/li&gt;
&lt;li&gt;a different image tag&lt;/li&gt;
&lt;li&gt;a config generator timing change&lt;/li&gt;
&lt;li&gt;a probe that turns green too early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The blast radius comes from the serving topology, not from how many lines changed.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hostPort&lt;/code&gt; DaemonSets are not bad. They are just less forgiving than the default Kubernetes workload patterns people get used to.&lt;/p&gt;

&lt;p&gt;If you use one for a public service, the safe mental model is not &quot;Kubernetes will roll this safely for me.&quot; The safe mental model is &quot;Kubernetes will do exactly what I asked, so I need to make sure I asked for a promotion flow that deserves production traffic.&quot;&lt;/p&gt;

&lt;p&gt;That is why I now treat the test DaemonSet as part of the design, not a nice-to-have around the design.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2775162202148449809'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/2775162202148449809'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/the-problem-with-hostport-daemonsets-in.html' title='The Problem With hostPort DaemonSets in Production, and How I Test Them Safely'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjj2eU0jlgsggmVzRNwg8lzRNTIk2C5A87XPCspX9AkhKRJXq-nMbMlkeKBE-q6THKaWrIHoLaE0CD_jxJWMMQQxs9mYVhilJXioCHcEHUkdv-gfohECRHj4LsWF8LFS4hYtCUG_ZUT2YxilxSpxNOPBWYeCHHe9zrcNnW58rpkXcDPRI8tTgcp4IWAJloj=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-1538783120151815948</id><published>2026-04-22T09:05:00.002+02:00</published><updated>2026-05-04T11:25:10.065+02:00</updated><title type='text'>How Docker Registry Garbage Collection Broke My Multi-Arch Images</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How Docker Registry Garbage Collection Broke My Multi-Arch Images --&gt;
&lt;!-- Suggested labels: Docker, Docker Registry, Kubernetes, ARM64, AMD64, DevOps, Series --&gt;
&lt;!-- Suggested meta description: A practical explanation of how Docker registry garbage collection broke live multi-architecture image tags, what the real failure mode was, and how I changed my cleanup rules afterward. --&gt;


&lt;p&gt;This article is part 10 of my MisLinux series on Kubernetes on Hetzner. It is based on a real failure in a self-hosted production registry I operate. I am not affiliated with Docker, CNCF, or any vendor mentioned here.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Registry garbage collection cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiiQJ_69aQYeAKMg7zg9cG61sFiDvz_1NorbGs_xrTpPlir_OOBUkTxODDKmAxLavQq48kCq39Nz-zpMPQ2WRIdqu4Tofo4wfWDURGtb6QZRdkv26bWivw3fKe8R5wKfSEp0mwk1Aqj1IvXPmNqLf32g1lVGHQT7LLONSvdWjSj03aIz84ybm2Jayzan5Lt&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Multi-architecture images are one of those things that feel clean in theory and treacherous in practice.&lt;/p&gt;

&lt;p&gt;In theory, you publish one image tag, Kubernetes pulls the right platform-specific manifest for each node, and everything is elegant. In practice, that elegance depends on the registry handling manifest lists, referenced sub-manifests, and garbage collection exactly the way you think it does.&lt;/p&gt;

&lt;p&gt;Mine did not.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Registry tag and manifest relationship chart&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEj6jWETtbt0Dolow8JPzxjCbwGHvMjiDhvIWT7DiU34cWY8PeHVXp1CzOauxeowqtv425jr4hu5ePXCiDPU5U2L83oP0icZHGVbm18M8Kanda92x9JLiseZi0Uv17DughMyitX1WVITiHbJwAbq7UNjjrH2RLGvuWSJwsllGJnqYKC_e31VY7wDYdQjmxZu&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;The incident in one sentence&lt;/h2&gt;

&lt;p&gt;I ran garbage collection on a self-hosted Docker registry and ended up with a perfectly normal-looking multi-arch tag whose platform-specific image manifests had been swept out from under it.&lt;/p&gt;

&lt;p&gt;That is the kind of breakage that destroys confidence because it does not look broken at first glance. The tag can still exist. The manifest list can still exist. But a real pull on one architecture fails because the child manifest or referenced blobs are gone.&lt;/p&gt;

&lt;h2&gt;Why this was not just &quot;I deleted the wrong thing&quot;&lt;/h2&gt;

&lt;p&gt;The important nuance is that this was not simply an operator typo. It was a mismatch between what I assumed garbage collection would protect and what the registry implementation actually considered reachable.&lt;/p&gt;

&lt;p&gt;I had a manifest list like this in conceptual terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;linux/amd64&lt;/code&gt; child manifest&lt;/li&gt;
&lt;li&gt;&lt;code&gt;linux/arm64&lt;/code&gt; child manifest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake was assuming the top-level tag was enough protection for the architecture-specific manifests beneath it. That assumption is intuitive. It is also operationally dangerous if your registry version or GC behavior does not traverse those references the way you expect.&lt;/p&gt;

&lt;p&gt;In my case, GC reclaimed the platform-specific manifests even though the higher-level multi-arch tag still existed.&lt;/p&gt;

&lt;h2&gt;Why this hurt more than a normal image mistake&lt;/h2&gt;

&lt;p&gt;If a single-architecture tag breaks, the blast radius is often obvious. The affected workload on the affected architecture fails quickly. You investigate the exact tag.&lt;/p&gt;

&lt;p&gt;Multi-arch breakage is more deceptive because different nodes can see different realities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one architecture may still pull successfully because it already has the image cached&lt;/li&gt;
&lt;li&gt;another architecture may fail on the next real pull&lt;/li&gt;
&lt;li&gt;a running deployment may look healthy until a rollout, restart, or node replacement happens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That delay is what makes the failure expensive. The registry problem is created earlier than it is discovered.&lt;/p&gt;

&lt;h2&gt;The cleanup assumption I had to unlearn&lt;/h2&gt;

&lt;p&gt;I had been treating some older immutable build tags too casually because of their naming style. In my environment, the Rocker-derived tags look like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0_&amp;lt;commit&amp;gt;_&amp;lt;commit&amp;gt;_&amp;lt;timestamp&amp;gt;_--no-wait&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At a glance, they look disposable. They are not.&lt;/p&gt;

&lt;p&gt;Those tags encode source-commit identity for the build inputs. The stale &lt;code&gt;--no-wait&lt;/code&gt; suffix is historical baggage from an old Satis-related flag, not evidence that the tag is transient. Once I re-centered on that fact, the cleanup policy had to change. The question stopped being &quot;which ugly tags look safe to prune?&quot; and became &quot;which tags are still protecting manifests that matter?&quot;&lt;/p&gt;

&lt;h2&gt;What I changed in the cleanup rules&lt;/h2&gt;

&lt;p&gt;My cleanup rule is stricter now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;never assume a weird-looking immutable tag is disposable&lt;/li&gt;
&lt;li&gt;never assume a &lt;code&gt;latest&lt;/code&gt; manifest list fully protects its child manifests&lt;/li&gt;
&lt;li&gt;keep direct architecture-specific tags for manifests that back a live multi-arch tag&lt;/li&gt;
&lt;li&gt;prefer reachability- and deployment-aware cleanup over name-based cleanup&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice, that means a safe cleanup process should understand at least three things:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Question&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Is this tag the current &lt;code&gt;latest&lt;/code&gt;?&lt;/td&gt;&lt;td&gt;&lt;code&gt;latest&lt;/code&gt; still matters operationally in many environments, even when I prefer digest pinning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Is this digest currently referenced by a live Deployment, DaemonSet, CronJob, or Job?&lt;/td&gt;&lt;td&gt;Deleting its tags may not break the running pod immediately, but it removes the safe pull path for restart or scale-out&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Is this immutable tag the only direct reference protecting a child manifest under a multi-arch tag?&lt;/td&gt;&lt;td&gt;Removing it can silently corrupt the apparent safety of the multi-arch tag&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This is more complex than &quot;keep the N newest tags,&quot; but complexity is better than pretending registry cleanup is harmless.&lt;/p&gt;

&lt;h2&gt;Why self-hosting a registry changes the standard advice&lt;/h2&gt;

&lt;p&gt;On a managed registry, a lot of this implementation detail disappears behind a service boundary. On a self-hosted registry, especially one that has already seen aggressive cleanup or storage pressure, you own the consequences directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;disk pressure&lt;/li&gt;
&lt;li&gt;GC semantics&lt;/li&gt;
&lt;li&gt;manifest reachability assumptions&lt;/li&gt;
&lt;li&gt;downtime risk from re-pulls&lt;/li&gt;
&lt;li&gt;differences between cached images and actually pullable images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not make self-hosting wrong. It just means registry maintenance is not janitorial work. It is production reliability work.&lt;/p&gt;

&lt;h2&gt;The verification step I now treat as mandatory&lt;/h2&gt;

&lt;p&gt;The most important new habit is manifest inspection before and after cleanup.&lt;/p&gt;

&lt;p&gt;I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the top-level tag points to&lt;/li&gt;
&lt;li&gt;which architecture descriptors exist under it&lt;/li&gt;
&lt;li&gt;whether those per-arch manifests also have direct tags&lt;/li&gt;
&lt;li&gt;whether the live cluster is still referencing an older digest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I cannot answer those questions quickly, I do not trust the cleanup operation.&lt;/p&gt;

&lt;p&gt;The broader lesson is that registries are not just blob stores. They are graph stores with operational consequences. When GC runs, it is traversing a reference graph. If your mental model of that graph is wrong, the cleanup can be &quot;successful&quot; and still destroy a production-safe pull path.&lt;/p&gt;

&lt;h2&gt;Why this changed my view of image hygiene&lt;/h2&gt;

&lt;p&gt;Before this incident, I thought of registry cleanup mostly as a disk-usage discipline problem. Afterward, I think of it as a correctness problem first and a storage problem second.&lt;/p&gt;

&lt;p&gt;Of course the registry needs cleanup. A self-hosted registry cannot grow forever. But correctness has to come first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live tags must remain pullable&lt;/li&gt;
&lt;li&gt;immutable provenance tags must not be treated as trash&lt;/li&gt;
&lt;li&gt;multi-arch child manifests must stay directly protected&lt;/li&gt;
&lt;li&gt;cleanup heuristics must reflect deployment reality, not naming aesthetics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that means the registry stays a little fatter until the logic is right, that is a better trade than recovering from a broken production image path.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;The phrase &quot;garbage collection&quot; sounds like maintenance. In a production registry, it is really a graph-deletion operation with platform-specific consequences.&lt;/p&gt;

&lt;p&gt;That is the real lesson I took from this failure: a multi-arch tag is only as safe as the child manifests and tags that still exist behind it. If your cleanup policy cannot prove those relationships before it deletes anything, it is not conservative enough yet.&lt;/p&gt;

&lt;p&gt;If you want the broader platform context for why I care so much about mixed ARM64 and AMD64 pull safety, read my earlier post on &lt;a href=&quot;https://mislinux.blogspot.com/2026/04/handling-multi-architecture-images-in.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;handling multi-architecture images in a mixed-architecture Kubernetes cluster&lt;/a&gt;. This post is the production failure mode that taught me what that architecture really costs in maintenance discipline.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1538783120151815948'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1538783120151815948'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/how-docker-registry-garbage-collection.html' title='How Docker Registry Garbage Collection Broke My Multi-Arch Images'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEiiQJ_69aQYeAKMg7zg9cG61sFiDvz_1NorbGs_xrTpPlir_OOBUkTxODDKmAxLavQq48kCq39Nz-zpMPQ2WRIdqu4Tofo4wfWDURGtb6QZRdkv26bWivw3fKe8R5wKfSEp0mwk1Aqj1IvXPmNqLf32g1lVGHQT7LLONSvdWjSj03aIz84ybm2Jayzan5Lt=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-8245086639592310282</id><published>2026-04-20T08:41:00.004+02:00</published><updated>2026-05-05T16:46:18.102+02:00</updated><title type='text'>Why My Prometheus History Suddenly Disappeared, and What I Changed Afterward</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Why My Prometheus History Suddenly Disappeared, and What I Changed Afterward --&gt;
&lt;!-- Suggested labels: Kubernetes, Prometheus, Grafana, Monitoring, Hetzner, DevOps, Series --&gt;
&lt;!-- Suggested meta description: A practical postmortem of how I lost historical Prometheus data on a small Kubernetes cluster, what caused it, and the retention and storage changes I made afterward. --&gt;


&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 20, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 9 of my MisLinux series on Kubernetes on Hetzner. Everything here comes from a real cluster I operate. I am not affiliated with or sponsored by Hetzner, Prometheus, Grafana Labs, or any other vendor mentioned in this post.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Prometheus history incident cover image&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhGhcVCgrxjg2asgr06SnzqBjdMNk1CWyZ5NIfwbSBna9SsX6REbfNfVVG2hJ4cp1cFTG6Bxj_qHW78oUjeilzCcy4T1NqhUBCQMiESp1zfKTg0lAyB2Y4kUIIjsU5kdLG6uS3CyxqEBOrw0xbmKR196QUxR0nmg_--vZI96lxVQU2pcfPDTDwvqHj9WwS3&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When monitoring feels stable for a while, it becomes easy to think of it as solved infrastructure. That is exactly when it tends to surprise you.&lt;/p&gt;

&lt;p&gt;I had a moment recently where Grafana suddenly stopped showing the historical Prometheus data I expected. The dashboards still loaded. Fresh data was still arriving. But the multi-day view that had been visible earlier was effectively gone. What made this more dangerous was that nothing had &quot;failed&quot; in the obvious sense: Prometheus was up, Grafana was up, and queries still returned current values.&lt;/p&gt;

&lt;p&gt;That kind of failure is worse than a full outage. A full outage is noisy. Silent history loss makes you think the system is working while the evidence you need for trend analysis, incident review, or storage growth comparisons has already disappeared.&lt;/p&gt;

&lt;div style=&quot;margin: 1.5em 0; text-align: center;&quot;&gt;&lt;img alt=&quot;Prometheus retention and TSDB size comparison&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEilf_XJy_R5w7psUm2v60d3_r-WI5EzrHfPFeiMdjg2hjn-o8MJhzumOr2yEk0S79LdRC0fqjHquB3spYEiOg67f_CsUJMP7IYntYkqPu6zCX0dgFU24U5RF5rM8teXQy4G93dCBTi4ZMtRmMnfUU7p1i1idGww9B4mT5i8chXg67p4F16iVD2_0cdW-_wO&quot; style=&quot;display: inline-block; height: auto; max-width: 100%;&quot; /&gt;&lt;/div&gt;

&lt;h2&gt;What I saw first&lt;/h2&gt;

&lt;p&gt;The symptom was simple: a Grafana dashboard that had shown several days of data earlier in the morning suddenly only had fresh points. It would have been easy to blame Grafana, because Grafana is where the problem became visible. But Grafana was just the messenger.&lt;/p&gt;

&lt;p&gt;The first useful clue was storage. The Prometheus PVC had dropped from a level that looked normal for a populated time-series database to something dramatically smaller. That told me the data had not merely become hard to query. It had actually been removed from the TSDB.&lt;/p&gt;

&lt;p&gt;The second clue was timing. This happened after a Prometheus recovery path that involved WAL replay and compaction pressure. In other words, this was not random data drift. It was a storage and retention interaction.&lt;/p&gt;

&lt;h2&gt;The actual cause&lt;/h2&gt;

&lt;p&gt;The root cause was the combination of two settings that looked individually reasonable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a small on-disk size cap for the Prometheus TSDB&lt;/li&gt;
&lt;li&gt;a retention target that implied more history than that size cap could safely hold after replay and compaction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice, Prometheus recovered, replayed WAL state, rebuilt blocks, and then immediately had to delete older blocks because the effective database size was now above the configured limit. From Prometheus&#39;s point of view, this was correct behavior. From an operator&#39;s point of view, it was a trap.&lt;/p&gt;

&lt;p&gt;The lesson is that retention by time and retention by size are not interchangeable. If you say &quot;keep N days&quot; but the size budget cannot physically hold N days of your scrape volume, the size cap wins. That is especially easy to underestimate on a cluster where monitoring scope gradually expands: more exporters, more labels, more synthetic metrics, more per-node detail, more per-service probes.&lt;/p&gt;

&lt;h2&gt;Why this is easy to miss on a small cluster&lt;/h2&gt;

&lt;p&gt;On a small Kubernetes cluster, it is tempting to be aggressive with Prometheus storage because you do not want the monitoring stack competing with production workloads. That instinct is correct. The mistake is assuming a low storage cap is safe just because the cluster itself is small.&lt;/p&gt;

&lt;p&gt;Small clusters often collect a surprisingly wide mix of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;host metrics from every node&lt;/li&gt;
&lt;li&gt;Kubernetes object state&lt;/li&gt;
&lt;li&gt;ingress and application health checks&lt;/li&gt;
&lt;li&gt;Pushgateway-fed metrics from cron jobs and watchdogs&lt;/li&gt;
&lt;li&gt;custom operational counters that answer &quot;did this thing actually happen?&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Individually, none of these feel large. Together, they create a TSDB shape that grows in bursts and compacts in ways that are hard to reason about from intuition alone.&lt;/p&gt;

&lt;h2&gt;What I changed afterward&lt;/h2&gt;

&lt;p&gt;I changed two things.&lt;/p&gt;

&lt;p&gt;First, I stopped treating the storage limit as a rough guess. I treated it as a capacity contract. If I wanted multi-day history for the metrics that matter operationally, the size cap had to reflect real ingestion and compaction behavior, not a convenient round number.&lt;/p&gt;

&lt;p&gt;Second, I separated the question of &lt;strong&gt;how much history I want&lt;/strong&gt; from &lt;strong&gt;how much history I can afford&lt;/strong&gt;. Those are related, but they are not the same design decision.&lt;/p&gt;

&lt;p&gt;My post-incident rule now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep enough retention to support real incident comparison, not aspirational observability&lt;/li&gt;
&lt;li&gt;size the TSDB for the real metric set plus recovery headroom&lt;/li&gt;
&lt;li&gt;verify what happens after restart and replay, not only during steady-state uptime&lt;/li&gt;
&lt;li&gt;watch disk trends for Prometheus specifically instead of assuming Grafana will expose the problem early enough&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The operational mistake I would avoid next time&lt;/h2&gt;

&lt;p&gt;The mistake was not &quot;using a small disk.&quot; The mistake was assuming a one-time retention choice would stay valid while the monitoring system kept evolving.&lt;/p&gt;

&lt;p&gt;Prometheus configuration often grows in operationally healthy ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you add a better exporter&lt;/li&gt;
&lt;li&gt;you add a new per-service alert&lt;/li&gt;
&lt;li&gt;you add a custom metric for a problem that used to be invisible&lt;/li&gt;
&lt;li&gt;you add a standby or external watchdog path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one makes the monitoring better. Each one also changes the storage profile. If you do not revisit the retention budget after those improvements, you eventually get a misleadingly &quot;healthy&quot; monitoring stack that cannot retain the history your dashboards imply.&lt;/p&gt;

&lt;h2&gt;What I now consider the minimum safe review loop&lt;/h2&gt;

&lt;p&gt;If I were setting this up again on a small cluster, I would review these four things together instead of separately:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Check&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TSDB size cap vs. actual usage&lt;/td&gt;&lt;td&gt;Prevents size-based retention from silently defeating the intended history window&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WAL replay behavior after restart&lt;/td&gt;&lt;td&gt;Reveals whether recovery itself can trigger aggressive block deletion&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Per-target scrape scope&lt;/td&gt;&lt;td&gt;Keeps metric growth tied to actual operational value&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dashboard time ranges people rely on&lt;/td&gt;&lt;td&gt;Prevents the UI from implying more retained history than Prometheus can really provide&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This is not glamorous work, but it is the kind of detail that decides whether your monitoring helps during an incident or merely looks professional between incidents.&lt;/p&gt;

&lt;h2&gt;What this changed in how I think about monitoring&lt;/h2&gt;

&lt;p&gt;Before this, I already believed a small cluster should run lean monitoring. After this, I think lean monitoring has to include lean &lt;strong&gt;retention design&lt;/strong&gt;, not only lean scrape configuration.&lt;/p&gt;

&lt;p&gt;It is easy to spend time tuning exporters and alert rules while leaving storage behavior as a default plus one arbitrary disk number. That is not enough. The retention model is part of the monitoring design, not a background implementation detail.&lt;/p&gt;

&lt;p&gt;That is also why I prefer practical monitoring over &quot;collect everything.&quot; The more honest question is not &quot;can I scrape this?&quot; It is &quot;will I still trust this system after restart, replay, compaction, and a week of real production behavior?&quot;&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;Losing Prometheus history was frustrating, but it was also clarifying. It forced me to stop thinking of monitoring data as something that passively accumulates and start treating it like any other production storage system with real capacity and lifecycle constraints.&lt;/p&gt;

&lt;p&gt;If you run your own small cluster, my recommendation is simple: test the recovery path, inspect the size-based retention interaction, and do not wait for a missing graph to tell you your monitoring history was more fragile than you thought.&lt;/p&gt;

&lt;p&gt;If you want the broader monitoring architecture around this story, read my earlier post on &lt;a href=&quot;https://mislinux.blogspot.com/2026/04/lean-monitoring-for-small-kubernetes.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;lean monitoring for a small Kubernetes cluster on Hetzner&lt;/a&gt;. This article is the more specific follow-up: what happened once that monitoring stack had to survive real operational pressure rather than just look correct on day one.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8245086639592310282'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8245086639592310282'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/why-my-prometheus-history-suddenly.html' title='Why My Prometheus History Suddenly Disappeared, and What I Changed Afterward'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEhGhcVCgrxjg2asgr06SnzqBjdMNk1CWyZ5NIfwbSBna9SsX6REbfNfVVG2hJ4cp1cFTG6Bxj_qHW78oUjeilzCcy4T1NqhUBCQMiESp1zfKTg0lAyB2Y4kUIIjsU5kdLG6uS3CyxqEBOrw0xbmKR196QUxR0nmg_--vZI96lxVQU2pcfPDTDwvqHj9WwS3=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-8615568228659322046</id><published>2026-04-07T14:38:00.002+02:00</published><updated>2026-05-05T16:46:05.104+02:00</updated><title type='text'>Handling Multi-Architecture Images in a Kubernetes Cluster Where Nodes Run Different CPU Architectures</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Handling Multi-Architecture Images in a Kubernetes Cluster Where Nodes Run Different CPU Architectures --&gt;
&lt;!-- Suggested labels: Kubernetes, Docker, ARM64, DevOps, Hetzner, Series --&gt;
&lt;!-- Suggested meta description: A practical guide to building and maintaining multi-architecture container images for a Kubernetes cluster that mixes ARM64 and AMD64 nodes, including the manifest-merging workflow that keeps both architectures in sync. --&gt;
&lt;img alt=&quot;09-multi-architecture-images-in-kubernetes&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgvE8rseELci0-RU9zyJgR7sl8_R-k5hErxkyopStjdt4Fy55UyzFwk6bRZ3OdxCfAA3St4h-Yr--ImZ_tImPyNF4dXkypufoyV_6mxwfvhkMe9TvuV3w4DRrrfEf-bPHmSDFJ_mhEGdksp4L8F7P4q92YWBdB0hBDi_kPuNcs7H9xYbdNBJ2rd_m2tAiLX&quot; style=&quot;display: block; height: auto; margin: 0 auto 1.5em auto; max-width: 1200px; width: 100%;&quot; /&gt;

&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 7, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part of my MisLinux series on building and operating a small Kubernetes cluster on Hetzner. It reflects my own hands-on experience and opinions, and I am not affiliated with, sponsored by, or endorsed by Hetzner, Docker, or any tool mentioned here.&lt;/p&gt;

&lt;p&gt;When I started building my Kubernetes cluster on Hetzner, I chose the ARM64-based &lt;code&gt;cax&lt;/code&gt; instances because they offered a better price-to-performance ratio than comparable AMD64 options. That seemed straightforward at the time. A few months later, it became complicated when I needed to rebuild container images from my AMD64 development laptop and push them to a private registry used by an all-ARM64 cluster.&lt;/p&gt;

&lt;p&gt;This post is about how I handle multi-architecture container images in that environment, including a workflow for pushing a single-architecture update without destroying the other architecture in the registry.&lt;/p&gt;

&lt;h2&gt;Why architecture matters for container images&lt;/h2&gt;

&lt;p&gt;A container image is not architecture-neutral. When you build an image on an AMD64 machine with &lt;code&gt;docker build&lt;/code&gt;, you produce an AMD64 image. If a Kubernetes node running ARM64 tries to pull that image, the pull fails or the container crashes at startup.&lt;/p&gt;

&lt;p&gt;For a homogeneous cluster, this is not a concern. Every node is the same architecture, every image is built for that architecture, and nothing collides. But the moment you mix architectures, or build from a machine that does not match your cluster, you have a problem.&lt;/p&gt;

&lt;p&gt;The solution is a multi-architecture manifest. Instead of one image pointing to one set of layers, you create an OCI image index (often called a &quot;fat manifest&quot; or multi-arch manifest) that points to two or more architecture-specific images under the same tag. When a node pulls that tag, the registry serves the right variant for the node&#39;s architecture automatically.&lt;/p&gt;

&lt;h2&gt;How Docker buildx creates multi-arch images&lt;/h2&gt;

&lt;p&gt;Docker &lt;code&gt;buildx&lt;/code&gt; is the standard tool for building multi-architecture images. With a properly configured builder, you can target multiple platforms in a single build command:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/myimage:latest \
  --push \
  .&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This produces an image index at &lt;code&gt;registry.example.com/myimage:latest&lt;/code&gt; that contains two descriptors: one for &lt;code&gt;linux/amd64&lt;/code&gt; and one for &lt;code&gt;linux/arm64&lt;/code&gt;. Any node pulling that tag gets the right image for its architecture.&lt;/p&gt;

&lt;p&gt;There are two ways &lt;code&gt;buildx&lt;/code&gt; produces the non-native architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QEMU emulation&lt;/strong&gt;: slow, but works on any machine. The non-native architecture is emulated in software.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native cross-compilation&lt;/strong&gt;: fast, but requires a builder node of the target architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production-quality builds, QEMU emulation is often too slow to be practical for large images. The right long-term answer is a native builder for each target architecture.&lt;/p&gt;

&lt;h2&gt;The current setup: AMD64 laptop, ARM64 cluster&lt;/h2&gt;

&lt;p&gt;In my environment today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Kubernetes cluster runs exclusively on Hetzner &lt;code&gt;cax&lt;/code&gt; ARM64 nodes.&lt;/li&gt;
&lt;li&gt;I build images from an AMD64 development machine.&lt;/li&gt;
&lt;li&gt;There is no ARM64 build agent inside the cluster yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building &lt;code&gt;linux/arm64&lt;/code&gt; images from an AMD64 machine via QEMU is feasible for small images but impractically slow for images that compile Rust code or run a full test suite during the build. For those, I need a native ARM64 build.&lt;/p&gt;

&lt;p&gt;Until a native ARM64 builder is set up, the pragmatic solution is to build each architecture separately and merge the results.&lt;/p&gt;

&lt;h2&gt;Building for a single architecture and preserving the other&lt;/h2&gt;

&lt;p&gt;The scenario I face regularly is this: I have made a change that needs to be deployed now. The &lt;code&gt;linux/arm64&lt;/code&gt; image is already in the registry and working correctly. I want to build a new &lt;code&gt;linux/amd64&lt;/code&gt; image from my laptop and push it without removing the existing &lt;code&gt;linux/arm64&lt;/code&gt; descriptor from the registry.&lt;/p&gt;

&lt;p&gt;If I do a naive &lt;code&gt;docker buildx build --platform linux/amd64 --push&lt;/code&gt;, the tag is replaced with a single-arch manifest pointing only to AMD64. The ARM64 image is gone from that tag. Any ARM64 node that pulls the tag will fail.&lt;/p&gt;

&lt;p&gt;The fix is to use &lt;code&gt;docker buildx imagetools create&lt;/code&gt; to compose a new multi-arch index from the individual descriptors. Here is the workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Build and push the AMD64 image under a staging tag.&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker buildx build \
  --platform linux/amd64 \
  --tag registry.example.com/myimage:amd64-staging \
  --push \
  .&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Inspect the current multi-arch manifest to confirm the ARM64 descriptor is still there.&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker buildx imagetools inspect registry.example.com/myimage:latest&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This shows the image index with each architecture&#39;s digest. If the ARM64 descriptor is present, proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Merge the new AMD64 staging image with the existing ARM64 image into the target tag.&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker buildx imagetools create \
  --tag registry.example.com/myimage:latest \
  registry.example.com/myimage:amd64-staging \
  registry.example.com/myimage:latest@sha256:&amp;lt;arm64-digest&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;imagetools create&lt;/code&gt; command reads the listed sources and produces a new image index containing all their descriptors. Referencing the ARM64 image by its digest guarantees you get exactly the version you inspected, not whatever might be at the tag at the moment the command runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Verify the result.&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker buildx imagetools inspect registry.example.com/myimage:latest&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Confirm both &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt; descriptors are present. Then delete the staging tag to keep the registry clean.&lt;/p&gt;

&lt;h2&gt;Why the registry itself can cause problems&lt;/h2&gt;

&lt;p&gt;One thing I learned the hard way is that not every registry handles multi-arch manifests cleanly. In my environment, I run a private registry backed by the standard Docker &lt;code&gt;registry:2&lt;/code&gt; image. Under aggressive garbage collection, OCI image indexes can be partially invalidated even when the blobs they reference still exist.&lt;/p&gt;

&lt;p&gt;The symptom is a tag that appears in the registry but returns a manifest parsing error or a missing-layer error when you try to pull it. The root cause is usually that GC removed a blob referenced by the manifest, or that a tag was rewritten in a format the registry did not store cleanly.&lt;/p&gt;

&lt;p&gt;A few things that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run garbage collection during low-traffic periods.&lt;/li&gt;
&lt;li&gt;Validate multi-arch tags immediately after a push by running &lt;code&gt;docker manifest inspect&lt;/code&gt; from a separate machine.&lt;/li&gt;
&lt;li&gt;Keep a backup of the digest for each architecture before doing any manifest manipulation. Digests are content-addressed and immutable; you can always reconstruct a tag from them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The long-term goal: native ARM64 builds in CI&lt;/h2&gt;

&lt;p&gt;Building ARM64 images from QEMU on an AMD64 laptop is a workaround, not a permanent solution. The right answer is a native ARM64 build node.&lt;/p&gt;

&lt;p&gt;The Hetzner &lt;code&gt;cax&lt;/code&gt; instances I already use in the cluster are ARM64. A &lt;code&gt;cax21&lt;/code&gt; instance costs roughly a few euros per month and can function as a &lt;code&gt;docker buildx&lt;/code&gt; builder node using the &lt;code&gt;remote&lt;/code&gt; or &lt;code&gt;kubernetes&lt;/code&gt; buildx driver. Once that is in place, a CI pipeline can build both architectures natively and push a proper multi-arch manifest in a single step, without any manual digest tracking.&lt;/p&gt;

&lt;p&gt;Until then, the staging-and-merge workflow described above is the practical path for keeping both architectures available under the same tag.&lt;/p&gt;

&lt;h2&gt;Summary&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Container images are architecture-specific; a multi-arch manifest lets one tag serve both ARM64 and AMD64 nodes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker buildx build --platform linux/amd64,linux/arm64&lt;/code&gt; builds both at once, but requires either QEMU or a native builder for the non-native architecture.&lt;/li&gt;
&lt;li&gt;When updating only one architecture, use &lt;code&gt;docker buildx imagetools create&lt;/code&gt; to merge the new descriptor with the preserved descriptor from the other architecture.&lt;/li&gt;
&lt;li&gt;Always reference the other architecture by digest, not by tag, during the merge.&lt;/li&gt;
&lt;li&gt;Validate the result immediately after pushing; registry GC and format mismatches can leave a tag broken without obvious errors.&lt;/li&gt;
&lt;li&gt;The permanent solution is a native builder for each target architecture.&lt;/li&gt;
&lt;/ul&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8615568228659322046'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/8615568228659322046'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/handling-multi-architecture-images-in.html' title='Handling Multi-Architecture Images in a Kubernetes Cluster Where Nodes Run Different CPU Architectures'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEgvE8rseELci0-RU9zyJgR7sl8_R-k5hErxkyopStjdt4Fy55UyzFwk6bRZ3OdxCfAA3St4h-Yr--ImZ_tImPyNF4dXkypufoyV_6mxwfvhkMe9TvuV3w4DRrrfEf-bPHmSDFJ_mhEGdksp4L8F7P4q92YWBdB0hBDi_kPuNcs7H9xYbdNBJ2rd_m2tAiLX=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-7645825764132088988</id><published>2026-04-06T14:18:00.002+02:00</published><updated>2026-05-05T16:45:52.929+02:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Alerting"/><category scheme="http://www.blogger.com/atom/ns#" term="Grafana"/><category scheme="http://www.blogger.com/atom/ns#" term="Hetzner"/><category scheme="http://www.blogger.com/atom/ns#" term="Kubernetes"/><category scheme="http://www.blogger.com/atom/ns#" term="Monitoring"/><category scheme="http://www.blogger.com/atom/ns#" term="Prometheus"/><category scheme="http://www.blogger.com/atom/ns#" term="Series"/><category scheme="http://www.blogger.com/atom/ns#" term="Telegram"/><title type='text'>Lean Monitoring for a Small Kubernetes Cluster on Hetzner</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: Lean Monitoring for a Small Kubernetes Cluster on Hetzner --&gt;
&lt;!-- Suggested labels: Kubernetes, Hetzner, Monitoring, Prometheus, Grafana, Alerting, Telegram, Series --&gt;
&lt;!-- Suggested meta description: How I set up workable, low-overhead monitoring for a self-managed Kubernetes cluster on Hetzner, with Prometheus, Grafana, Telegram alerts, and an external watchdog running on a home NAS. --&gt;
&lt;img alt=&quot;08-lean-monitoring-for-a-small-kubernetes-cluster-on-hetzner&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjoi839CEnqmk-aYMQzV-4ppJkxknQALGUBlix0cZFT29ZE3WartV3m0mX7teU0MsC9vMODjjVWcm3pv-Bvnqux64w5V14MxsqqOL8XrzemEvc7CXnbqFShqFJbvZxhugUiwRwGskMbgM8iYKUU7m0ZYOXe_On53ThgvTLZ3Cqxi58B_37z-weE52URoQC2&quot; style=&quot;display: block; height: auto; margin: 0 auto 1.5em auto; max-width: 1200px; width: 100%;&quot; /&gt;

&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 6, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 8 of my MisLinux series on Kubernetes on Hetzner. All configurations described here are based on my own setup and real operational experience. I am not affiliated with or sponsored by Hetzner, Grafana Labs, or any other tool mentioned.&lt;/p&gt;

&lt;p&gt;A cluster that runs without visibility is just a time bomb with a pleasant uptime. After getting the basics working on a small Hetzner k3s cluster, the next practical step is knowing whether the services running on it are actually healthy, and getting a notification before a problem becomes an incident.&lt;/p&gt;

&lt;p&gt;This post covers how I built a lean monitoring stack that fits comfortably on modest virtual machines without consuming the CPU or disk budget that the actual workloads need.&lt;/p&gt;

&lt;h2&gt;What I wanted from monitoring&lt;/h2&gt;

&lt;p&gt;Before choosing tools I wrote down a short list of practical questions I needed answered without manual inspection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is each workload responding? Do the health checks pass?&lt;/li&gt;
&lt;li&gt;Are pods crashing or restarting unexpectedly?&lt;/li&gt;
&lt;li&gt;Is any node running low on disk space?&lt;/li&gt;
&lt;li&gt;Are requests being served, or has traffic dropped to zero?&lt;/li&gt;
&lt;li&gt;Did a new node join the cluster correctly?&lt;/li&gt;
&lt;li&gt;Is the external watchdog alive and checking in?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also wanted a notification channel I could use from a phone without installing anything complicated. Telegram works well for this.&lt;/p&gt;

&lt;h2&gt;The stack: Prometheus, Grafana, and node_exporter&lt;/h2&gt;

&lt;p&gt;For a small cluster, the standard combination of Prometheus (metrics collection), node_exporter (host-level metrics per node), and Grafana (dashboards and alerting) is hard to beat. It is widely documented, has reasonable resource consumption when tuned carefully, and integrates directly with Kubernetes through service discovery.&lt;/p&gt;

&lt;h3&gt;Resource discipline&lt;/h3&gt;

&lt;p&gt;On modest hardware, a few settings matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prometheus scrape interval&lt;/strong&gt;: I use 60 seconds rather than the default 15 seconds. For a services-focused cluster this is more than adequate. Disk write load drops sharply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention&lt;/strong&gt;: I keep 15 days of raw data. Beyond that, Grafana&#39;s visualization has diminishing returns for the day-to-day questions I actually ask. Prometheus compacts data automatically; I just make sure the retention flag is set explicitly rather than relying on the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;node_exporter collectors&lt;/strong&gt;: I disable the collectors I do not use: &lt;code&gt;bcache&lt;/code&gt;, &lt;code&gt;drbd&lt;/code&gt;, &lt;code&gt;infiniband&lt;/code&gt;, and several others that generate irrelevant metrics for a plain virtual machine. The &lt;code&gt;--collector.disable-defaults&lt;/code&gt; flag and selective re-enables keep the cardinality low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grafana data retention&lt;/strong&gt;: Grafana itself stores dashboards and alert state but not time-series data. The dashboards are kept in a ConfigMap so they survive pod restarts without a separate persistent volume.&lt;/p&gt;

&lt;h3&gt;What gets scraped&lt;/h3&gt;

&lt;p&gt;The standard target set for a k3s cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node_exporter&lt;/code&gt; on each node (CPU, memory, disk, network)&lt;/li&gt;
&lt;li&gt;k3s itself exposes kubelet and API server metrics on standard ports&lt;/li&gt;
&lt;li&gt;Any application that exposes a &lt;code&gt;/metrics&lt;/code&gt; endpoint gets added to a ServiceMonitor or static scrape config&lt;/li&gt;
&lt;li&gt;A custom Pushgateway for metrics that cannot be scraped directly (batch jobs, external health checks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CPU usage per node, &lt;code&gt;node_cpu_seconds_total&lt;/code&gt; with &lt;code&gt;rate()&lt;/code&gt; over 5 minutes gives a smooth enough signal to detect saturation without false alarms on short spikes.&lt;/p&gt;

&lt;h2&gt;Dashboards: one unified view&lt;/h2&gt;

&lt;p&gt;I keep one main dashboard with panels for each concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node health: CPU, memory, disk per node side by side&lt;/li&gt;
&lt;li&gt;Pod restarts: a table showing any pod with more than 0 restarts in the last hour&lt;/li&gt;
&lt;li&gt;Ingress error rate: HTTP 4xx and 5xx rates from the nginx ingress controller&lt;/li&gt;
&lt;li&gt;Health endpoint status: custom panels that poll each service&#39;s &lt;code&gt;/health&lt;/code&gt; endpoint and show a green/red state&lt;/li&gt;
&lt;li&gt;Request rate: the requests-per-second metric from the main production service, shown as a time-series so drops are immediately visible&lt;/li&gt;
&lt;li&gt;Bandwidth: inbound and outbound bytes per node, useful for estimating how close you are to the included monthly traffic allocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also keep individual per-service dashboards linked from the main one. This keeps the top-level view clean while allowing a drill-down when something looks wrong.&lt;/p&gt;

&lt;h3&gt;Bandwidth and cost tracking&lt;/h3&gt;

&lt;p&gt;Hetzner includes a monthly traffic limit per server. Exceeding it has cost implications. I track &lt;code&gt;node_network_transmit_bytes_total&lt;/code&gt; on the public interface for each node, compute a monthly rate using Prometheus&#39;s &lt;code&gt;increase()&lt;/code&gt; function, and display it as a panel alongside a threshold line.&lt;/p&gt;

&lt;p&gt;For cost, I pull the daily Hetzner billing data through their API and push it via Pushgateway. This gives a simple bar chart of daily spend, which is useful for catching unexpected charges early.&lt;/p&gt;

&lt;h2&gt;Alerting: Grafana rules with Telegram delivery&lt;/h2&gt;

&lt;p&gt;Grafana&#39;s built-in alerting can evaluate PromQL expressions and send notifications. I use it instead of the Prometheus Alertmanager to keep the stack simpler.&lt;/p&gt;

&lt;p&gt;Telegram is a convenient delivery channel: no email server required, messages arrive on mobile immediately, and the Bot API is straightforward to configure. In Grafana: add a Contact Point of type Telegram, paste the bot token and chat ID, and notifications start flowing.&lt;/p&gt;

&lt;h3&gt;What alerts I actually run&lt;/h3&gt;

&lt;p&gt;Keeping the alert list short and meaningful avoids alarm fatigue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disk above 80%&lt;/strong&gt; on any node — gives time to act before a full disk causes silent failures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No requests in the last 30 minutes&lt;/strong&gt; on any production service that should have traffic — this catches outages that would otherwise go unnoticed overnight&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod restart count increasing&lt;/strong&gt; — anything more than 2 restarts in an hour gets flagged&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health endpoint not returning 200&lt;/strong&gt; — a simple up/down alert for each public service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External watchdog not checking in&lt;/strong&gt; — see the next section&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Writing alert messages that explain themselves&lt;/h3&gt;

&lt;p&gt;The most useful discipline I adopted was writing alert messages as if the reader has never seen the system before. Each message includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where it comes from (which cluster, which service)&lt;/li&gt;
&lt;li&gt;What it means in plain terms (the service is down / latency is elevated / traffic fell unexpectedly)&lt;/li&gt;
&lt;li&gt;What to check first (a specific command, a specific URL, or a specific Kubernetes namespace)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A message that just says &quot;alert firing&quot; is not useful at 2am. A message that says &quot;Production VAST service on the Hetzner cluster is receiving zero requests — likely the nginx ingress pod has restarted. Check: &lt;code&gt;kubectl get pods -n production&lt;/code&gt;&quot; is actionable immediately.&lt;/p&gt;

&lt;h2&gt;The external watchdog: a check from outside the cluster&lt;/h2&gt;

&lt;p&gt;A monitoring stack that lives entirely inside the cluster it monitors has a structural blind spot: if the cluster loses connectivity, or if a misconfiguration blocks all traffic, the alerting fails silently too.&lt;/p&gt;

&lt;p&gt;I run a small Docker container on a home NAS device as an external watchdog. Its job is simple: poll each critical service URL every minute, and send a Telegram message if any check fails or times out. If the NAS container itself stops sending its periodic heartbeat to the cluster, the cluster alerts that the external watchdog is gone.&lt;/p&gt;

&lt;p&gt;This gives a two-way health check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NAS → Cluster&lt;/strong&gt;: the NAS polls the cluster from outside and alerts if service URLs stop responding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cluster → NAS&lt;/strong&gt;: the cluster checks whether the NAS watchdog has recently posted its heartbeat, and alerts if it has not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither direction alone is sufficient. Together they cover the main failure scenarios.&lt;/p&gt;

&lt;h3&gt;What the watchdog checks&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each public service URL (health endpoint, or a minimal response that confirms the service is alive)&lt;/li&gt;
&lt;li&gt;TLS certificate validity (flagging within 14 days of expiry)&lt;/li&gt;
&lt;li&gt;DNS resolution for production hostnames&lt;/li&gt;
&lt;li&gt;Response time — a timeout on connection or response is treated as a failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All checks include explicit timeouts: connection timeout separate from response timeout. A service that connects but hangs indefinitely is a different kind of problem than one that refuses the connection, and both need to be caught.&lt;/p&gt;

&lt;h2&gt;What I learned building this&lt;/h2&gt;

&lt;p&gt;A few things that took more than one attempt to get right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert thresholds need tuning based on real traffic patterns.&lt;/strong&gt; A &quot;no requests in 30 minutes&quot; alert needs to account for legitimate low-traffic periods (nights, weekends). I adjusted the alert to trigger only when traffic drops significantly below the rolling average for the same time window the previous day. This eliminates false alarms without hiding real outages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk alerts should fire early.&lt;/strong&gt; An 80% threshold sounds conservative but on a busy registry node with several large container images, it is easy to cross from 80% to 95% in hours. I also added a separate alert for fast-growing directories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Pushgateway is useful but needs housekeeping.&lt;/strong&gt; Metrics pushed via Pushgateway persist until explicitly deleted. A job that fails silently and stops pushing will show stale metrics rather than absence. I treat any Pushgateway metric older than 10 minutes for a frequently-running job as a signal of failure, not as the last known good value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource limits on Prometheus and Grafana pods matter.&lt;/strong&gt; Without explicit limits, Prometheus will happily consume several gigabytes of memory on a small node if scrape interval and retention are not tuned. I set memory requests and limits explicitly and check them after any scrape configuration change.&lt;/p&gt;

&lt;h2&gt;The result&lt;/h2&gt;

&lt;p&gt;After tuning, the monitoring stack runs comfortably alongside production workloads on modest Hetzner virtual machines. Dashboards load quickly. Alerts arrive within 2 minutes of an actual failure. The external watchdog has already caught two incidents where the internal cluster monitoring would have been silent.&lt;/p&gt;

&lt;p&gt;The key constraint I kept in mind throughout was that monitoring should cost less than what it protects. On a small cluster, that means being deliberate about scrape intervals, retention, and the number of metrics collected — not maximizing observability for its own sake.&lt;/p&gt;

&lt;p&gt;For a small self-managed cluster, this is enough. It answers the questions that matter, and it does not add a maintenance burden that competes with the actual work.&lt;/p&gt;

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;*Next in the series: Handling multi-architecture images in a Kubernetes cluster where nodes run different CPU architectures.*&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7645825764132088988'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/7645825764132088988'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/04/lean-monitoring-for-small-kubernetes.html' title='Lean Monitoring for a Small Kubernetes Cluster on Hetzner'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjoi839CEnqmk-aYMQzV-4ppJkxknQALGUBlix0cZFT29ZE3WartV3m0mX7teU0MsC9vMODjjVWcm3pv-Bvnqux64w5V14MxsqqOL8XrzemEvc7CXnbqFShqFJbvZxhugUiwRwGskMbgM8iYKUU7m0ZYOXe_On53ThgvTLZ3Cqxi58B_37z-weE52URoQC2=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-3700880289325829827</id><published>2026-03-28T12:42:00.003+01:00</published><updated>2026-05-05T16:45:41.017+02:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Claude"/><category scheme="http://www.blogger.com/atom/ns#" term="Documentation"/><category scheme="http://www.blogger.com/atom/ns#" term="Engineering"/><category scheme="http://www.blogger.com/atom/ns#" term="GitHub Copilot"/><category scheme="http://www.blogger.com/atom/ns#" term="OpenAI Codex"/><title type='text'>How I Use CIR Notes to Make AI Coding Agents More Useful</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: How I Use CIR Notes to Make AI Coding Agents More Useful --&gt;
&lt;!-- Suggested labels: AI, GitHub Copilot, Claude, OpenAI Codex, Documentation, Engineering --&gt;
&lt;!-- Suggested meta description: Why I document Context, Intent, and Rationale in AGENTS.md, repo instructions, and code comments so AI coding agents like Copilot, Claude, and Codex can make better decisions. --&gt;
&lt;img alt=&quot;07-how-i-use-cir-notes-to-make-ai-coding-agents-more-useful&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjnm4hceL6xo9BER19knBHBUF2HsovGeijYTS5TUxVQBwHb7NBBeFqjgGgDm8LPI-FNjzRNGS0VxgiX7BusJMbQ7WiXNr29j6qupVHSLhfbZfPHDJCjRUYPfsJkpkrc2PGSPkK8pqKSe21mOluXuywO3WljLle9DV-Es4131tQntOezD0yiOux1JG0CJ5Li&quot; style=&quot;display: block; height: auto; margin: 0 auto 1.5em auto; max-width: 1200px; width: 100%;&quot; /&gt;

&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: April 4, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This is a standalone MisLinux article about how I work with AI coding agents such as GitHub Copilot, Claude, and OpenAI Codex. It reflects my own workflow and opinions only; I am not affiliated with, sponsored by, or endorsed by Microsoft, Anthropic, or OpenAI.&lt;/p&gt;

&lt;p&gt;The most useful change I have made for AI-assisted development is not a newer model or a longer prompt. It is documenting Context, Intent, and Rationale close to the work itself.&lt;/p&gt;

&lt;p&gt;I usually shorten that to &lt;strong&gt;CIR&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; what environment, system state, history, or constraint already exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intent:&lt;/strong&gt; what I am trying to achieve right now&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rationale:&lt;/strong&gt; why this approach was chosen instead of another one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds simple, but it closes one of the biggest gaps in AI-generated code: the model often sees what the code does without clearly seeing why it was shaped that way.&lt;/p&gt;

&lt;h2&gt;Why tools like Copilot, Claude, and Codex still miss important things&lt;/h2&gt;

&lt;p&gt;Modern coding agents are genuinely useful. They can trace files, suggest patches, explain APIs, and move through a repository faster than a human can when the work is mechanical.&lt;/p&gt;

&lt;p&gt;But they still struggle in a few recurring situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a configuration looks strange for a good historical reason&lt;/li&gt;
&lt;li&gt;a seemingly redundant check exists because production failed without it&lt;/li&gt;
&lt;li&gt;an unpopular dependency was chosen to match another system&lt;/li&gt;
&lt;li&gt;a simple-looking refactor would quietly break deployment, compliance, or recovery workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When that reasoning only lives in a developer&#39;s head, an AI agent has to guess. Sometimes it guesses well. Sometimes it confidently &quot;improves&quot; the exact thing that was protecting the system.&lt;/p&gt;

&lt;h2&gt;The missing piece is rarely context alone&lt;/h2&gt;

&lt;p&gt;Many teams already write some context down. They might have a README, a ticket, or a comment saying what a script is for.&lt;/p&gt;

&lt;p&gt;That helps, but it is incomplete.&lt;/p&gt;

&lt;p&gt;The missing piece is usually the combination of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what already exists&lt;/li&gt;
&lt;li&gt;what change is intended&lt;/li&gt;
&lt;li&gt;why that change was implemented that way&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without the third point, AI tooling tends to optimize for local neatness instead of system truth.&lt;/p&gt;

&lt;p&gt;A guard clause looks ugly, so it gets removed. A duplicate-looking config path seems unnecessary, so it gets unified. A staging-only publish flow seems too specialized, so it gets generalized and breaks production.&lt;/p&gt;

&lt;p&gt;Rationale is what tells the agent, &quot;This is not an accident. Preserve the tradeoff unless you are deliberately changing it.&quot;&lt;/p&gt;

&lt;h2&gt;Why I layer CIR instead of keeping it in one place&lt;/h2&gt;

&lt;p&gt;I have found that AI agents work best when CIR exists in more than one layer.&lt;/p&gt;

&lt;h3&gt;1. Repo-level guidance&lt;/h3&gt;

&lt;p&gt;At the repository level, I like a file such as &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; or another top-level instructions document.&lt;/p&gt;

&lt;p&gt;That is where I keep things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the big-picture architecture&lt;/li&gt;
&lt;li&gt;expected deployment paths&lt;/li&gt;
&lt;li&gt;important external systems&lt;/li&gt;
&lt;li&gt;how changes are normally validated&lt;/li&gt;
&lt;li&gt;repo-wide CIR notes for recent design decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps the agent avoid making changes that are technically tidy but operationally wrong.&lt;/p&gt;

&lt;h3&gt;2. Directory-level &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Inside important directories, I like an &lt;code&gt;AGENTS.md&lt;/code&gt; that narrows the scope.&lt;/p&gt;

&lt;p&gt;That file is where I describe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what this directory is responsible for&lt;/li&gt;
&lt;li&gt;what kinds of changes are normal or risky here&lt;/li&gt;
&lt;li&gt;local naming rules and workflow expectations&lt;/li&gt;
&lt;li&gt;the Context / Intent / Rationale behind the current shape of that area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the layer that helps an agent stop treating every folder like generic code. A Blogger-content directory, a Terraform directory, and a Docker build directory may all be in the same repo, but they need different instincts.&lt;/p&gt;

&lt;h3&gt;3. CIR near the code itself&lt;/h3&gt;

&lt;p&gt;For important scripts, modules, classes, or tests, I like a short CIR block close to the code.&lt;/p&gt;

&lt;p&gt;That is where the most specific reasoning belongs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why this function exists&lt;/li&gt;
&lt;li&gt;which edge case it protects against&lt;/li&gt;
&lt;li&gt;why a weird branch is intentional&lt;/li&gt;
&lt;li&gt;which operational failure led to this implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is often the difference between an AI agent preserving a critical safeguard and deleting it because it looks redundant.&lt;/p&gt;

&lt;h2&gt;What this looks like in practice&lt;/h2&gt;

&lt;p&gt;If I only write one top-level instruction file, the agent gets broad awareness but misses local tradeoffs.&lt;/p&gt;

&lt;p&gt;If I only write comments in code, the agent may understand a function but miss the system around it.&lt;/p&gt;

&lt;p&gt;If I only write an &lt;code&gt;AGENTS.md&lt;/code&gt; in one folder, the agent may still miss the repo-wide deployment or compliance model.&lt;/p&gt;

&lt;p&gt;Layering the guidance works better because each level answers a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;repo instructions&lt;/strong&gt; explain the operating environment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;directory `AGENTS.md`&lt;/strong&gt; explains local expectations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;code-level CIR&lt;/strong&gt; explains why this implementation looks the way it does&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination reduces accidental &quot;helpful&quot; changes.&lt;/p&gt;

&lt;h2&gt;The benefit is not only for AI&lt;/h2&gt;

&lt;p&gt;The nice side effect is that humans benefit too.&lt;/p&gt;

&lt;p&gt;A future maintainer can skim the repo-level notes, move into the directory-level guidance, and then understand the code-level tradeoff without hunting through chat logs or old tickets.&lt;/p&gt;

&lt;p&gt;That matters because AI-assisted work creates a new failure mode: important design reasoning can end up trapped in one-off conversations with the tool instead of becoming part of the project memory.&lt;/p&gt;

&lt;p&gt;If the reasoning does not land in the repository, it might as well not exist.&lt;/p&gt;

&lt;h2&gt;How CIR changes the quality of AI edits&lt;/h2&gt;

&lt;p&gt;When I work this way, the output from coding agents improves in very practical ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they are less likely to remove deliberate safety checks&lt;/li&gt;
&lt;li&gt;they make fewer wrong assumptions about deployment or publishing flow&lt;/li&gt;
&lt;li&gt;they can explain their own changes in terms of the existing system&lt;/li&gt;
&lt;li&gt;they are more likely to preserve edge-case handling&lt;/li&gt;
&lt;li&gt;they require less corrective prompting from me afterward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, CIR turns the agent from a fast code generator into a better repository participant.&lt;/p&gt;

&lt;h2&gt;My rule of thumb&lt;/h2&gt;

&lt;p&gt;If a future engineer or AI agent could reasonably ask, &quot;Why on earth is it done this way?&quot;, I try to record the answer where the question will naturally arise.&lt;/p&gt;

&lt;p&gt;That usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repo-wide context near the repo root&lt;/li&gt;
&lt;li&gt;local intent in the relevant directory&lt;/li&gt;
&lt;li&gt;implementation rationale in or near the code itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not try to document everything. I try to document the decisions that would otherwise be silently lost.&lt;/p&gt;

&lt;h2&gt;Final thought&lt;/h2&gt;

&lt;p&gt;Copilot, Claude, and Codex are all more useful when they are not forced to reverse-engineer intent from raw files alone. The better I capture Context, Intent, and Rationale in the repository, the more often the agent makes a change that is not only syntactically correct, but operationally correct too.&lt;/p&gt;

&lt;p&gt;For me, that is the real productivity gain: not just faster edits, but fewer wrong edits.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/3700880289325829827'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/3700880289325829827'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/03/how-i-use-cir-notes-to-make-ai-coding.html' title='How I Use CIR Notes to Make AI Coding Agents More Useful'/><author><name>Vicente Arteaga Gómez</name><uri>http://www.blogger.com/profile/12086583828602126000</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjnm4hceL6xo9BER19knBHBUF2HsovGeijYTS5TUxVQBwHb7NBBeFqjgGgDm8LPI-FNjzRNGS0VxgiX7BusJMbQ7WiXNr29j6qupVHSLhfbZfPHDJCjRUYPfsJkpkrc2PGSPkK8pqKSe21mOluXuywO3WljLle9DV-Es4131tQntOezD0yiOux1JG0CJ5Li=s72-c" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4670815028761617050.post-1538585678430844784</id><published>2026-03-18T10:58:09.458+01:00</published><updated>2026-05-05T16:45:29.684+02:00</updated><title type='text'>What Kubernetes on Hetzner Costs, and When I Wouldn&#39;t Use It</title><content type='html'>&lt;!-- Generated Blogger body HTML fragment. Paste into Blogger&#39;s HTML editor. --&gt;
&lt;!-- Suggested Blogger title: What Kubernetes on Hetzner Costs, and When I Wouldn&#39;t Use It --&gt;
&lt;!-- Suggested labels: Kubernetes, Hetzner, Cost, DevOps, Strategy, Series --&gt;
&lt;!-- Suggested meta description: A realistic look at the cost and operational tradeoffs of Kubernetes on Hetzner, including cases where a simpler platform or different provider makes more sense. --&gt;
&lt;img alt=&quot;06-what-kubernetes-on-hetzner-costs-and-when-i-wouldnt-use-it&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgPesnj9pSG2o8AEujqRuSChPrxXgNBnurNAcNE3oR7Cv9DapTYUCH4r0HIvW4ppswbrElJHSoci_-2_JaGVLm3TfQaE-3r4RCMWnR7D9TLdlFct2KED1c60upURxy_rVFAGW8e14tMsVNM12QYo5hF8JXf1RNaFkNVethRP8vsgrtgWldqaj-YubfSQpUE&quot; style=&quot;display: block; height: auto; margin: 0 auto 1.5em auto; max-width: 1200px; width: 100%;&quot; /&gt;

&lt;div style=&quot;background: #f6f8fa; border-radius: 10px; border: 1px solid #d0d7de; margin: 0 0 1.4em 0; padding: 0.95em 1.05em;&quot;&gt;&lt;p style=&quot;font-size: 1.02em; line-height: 1.45; margin: 0;&quot;&gt;&lt;strong&gt;By Vicente Arteaga Gomez&lt;/strong&gt;&lt;/p&gt;&lt;p style=&quot;color: #57606a; font-size: 0.95em; line-height: 1.45; margin: 0.35em 0 0 0;&quot;&gt;MisLinux · Last updated: May 5, 2026&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;This article is part 6 of my MisLinux series on Kubernetes on Hetzner. It reflects my personal experience and tradeoff analysis only; I am not affiliated with, sponsored by, or endorsed by Hetzner.&lt;/p&gt;

&lt;p&gt;The strongest argument for running Kubernetes on Hetzner is not that it is the most feature-rich platform. It is that the cost-to-control ratio can be excellent for the right workload profile.&lt;/p&gt;

&lt;h2&gt;The question that matters more than the cheapest invoice&lt;/h2&gt;

&lt;p&gt;When I revisit the economics now, I ask a slightly different question than I did at the beginning:&lt;/p&gt;

&lt;p&gt;&amp;gt; What is the cheapest platform that still gives me a boring recovery story?&lt;/p&gt;

&lt;p&gt;That framing matters because infrastructure cost is not only node price. It is also the number of future incidents that force a skilled operator to stop everything else and debug the platform under pressure.&lt;/p&gt;

&lt;h2&gt;What the platform saves you&lt;/h2&gt;

&lt;p&gt;Compared with larger managed-cloud setups, Hetzner can keep infrastructure spend far more predictable. That matters when the cluster supports a real product but the team is still sensitive to recurring cost.&lt;/p&gt;

&lt;p&gt;The savings come from straightforward compute and infrastructure pricing, not from some hidden optimization trick. If your workloads are stable and your team can operate the cluster responsibly, the economics can be compelling.&lt;/p&gt;

&lt;h2&gt;Real Hetzner costs: what I actually pay&lt;/h2&gt;

&lt;p&gt;The table below reflects real Hetzner pricing (Falkenstein datacenter, April 2026) for a production cluster with one control-plane node, one general worker, and one dedicated workload node. These are list prices in EUR excluding VAT; your exact invoice will vary by datacenter, usage, and any discounts.&lt;/p&gt;

&lt;h3&gt;Compute (ARM64 &lt;code&gt;cax&lt;/code&gt; series — best price/performance for most workloads)&lt;/h3&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Node role&lt;/th&gt;&lt;th&gt;Machine type&lt;/th&gt;&lt;th&gt;vCPU&lt;/th&gt;&lt;th&gt;RAM&lt;/th&gt;&lt;th&gt;Monthly price&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Master / control plane&lt;/td&gt;&lt;td&gt;cax21&lt;/td&gt;&lt;td&gt;4 vCPU (ARM64)&lt;/td&gt;&lt;td&gt;8 GB&lt;/td&gt;&lt;td&gt;~€7.49&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;General worker&lt;/td&gt;&lt;td&gt;cax41&lt;/td&gt;&lt;td&gt;16 vCPU (ARM64)&lt;/td&gt;&lt;td&gt;32 GB&lt;/td&gt;&lt;td&gt;~€22.49&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dedicated workload node&lt;/td&gt;&lt;td&gt;cax21&lt;/td&gt;&lt;td&gt;4 vCPU (ARM64)&lt;/td&gt;&lt;td&gt;8 GB&lt;/td&gt;&lt;td&gt;~€7.49&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Compute subtotal&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;~€37.47&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h3&gt;Storage and network add-ons&lt;/h3&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Item&lt;/th&gt;&lt;th&gt;Spec&lt;/th&gt;&lt;th&gt;Monthly price&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Block volumes (7 PVCs, ~170 GB total)&lt;/td&gt;&lt;td&gt;€0.0571/GB/month&lt;/td&gt;&lt;td&gt;~€9.72&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Primary IPv4 addresses (3 nodes)&lt;/td&gt;&lt;td&gt;€0.50/IP/month&lt;/td&gt;&lt;td&gt;~€1.50&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Server backup for master node&lt;/td&gt;&lt;td&gt;20% of server price&lt;/td&gt;&lt;td&gt;~€1.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Add-ons subtotal&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;~€12.82&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h3&gt;Total: ~€50–60/month for a real three-node production cluster&lt;/h3&gt;

&lt;p&gt;The full monthly cost including compute, storage, IPs, and backups is approximately &lt;strong&gt;€50–60/month&lt;/strong&gt; depending on storage usage. This compares directly against:&lt;/p&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Comparable setup&lt;/th&gt;&lt;th&gt;Approximate monthly cost&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Hetzner (self-managed k3s)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3 nodes, 7 PVCs, 1 load balancer&lt;/td&gt;&lt;td&gt;~€50–60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GKE Autopilot&lt;/td&gt;&lt;td&gt;3-node equivalent&lt;/td&gt;&lt;td&gt;~€150–250&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EKS (AWS)&lt;/td&gt;&lt;td&gt;3 × t3.large + EBS + ELB&lt;/td&gt;&lt;td&gt;~€200–300&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DigitalOcean DOKS&lt;/td&gt;&lt;td&gt;3 × basic-2vCPU-4GB + volumes&lt;/td&gt;&lt;td&gt;~€90–120&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fly.io / Render&lt;/td&gt;&lt;td&gt;Comparable compute&lt;/td&gt;&lt;td&gt;~€80–150&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The Hetzner number is real; the others are estimates based on list prices for comparable node sizes. All managed Kubernetes providers include control-plane costs that Hetzner does not charge for directly — you pay for that with operator time instead.&lt;/p&gt;

&lt;h2&gt;The command trail behind the monthly numbers&lt;/h2&gt;

&lt;p&gt;The reason I trust cost notes more when they are written down is that the verification path is concrete:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# Inventory the live Hetzner shape
hcloud server list
hcloud volume list

# Check what the cluster is actually using
kubectl get nodes -o wide
kubectl get pvc -A&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The kind of output I want from that first command is something I can reason about quickly:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;master-node      cax21
worker-node-0    cax41
vpaidd-worker-0  cax21&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Even when I later graph or summarize the costs, those are still the commands that keep the cost discussion grounded in real infrastructure instead of in generic provider marketing.&lt;/p&gt;

&lt;h2&gt;What the platform asks from you in return&lt;/h2&gt;

&lt;p&gt;The lower infrastructure cost is balanced by higher operational ownership.&lt;/p&gt;

&lt;p&gt;You are taking responsibility for things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;node configuration and cloud-init templates&lt;/li&gt;
&lt;li&gt;cluster networking assumptions (Flannel VXLAN, firewall rules)&lt;/li&gt;
&lt;li&gt;firewall correctness (both Hetzner Cloud Firewall and in-cluster network policies)&lt;/li&gt;
&lt;li&gt;storage strategy (PVCs, backup, restore)&lt;/li&gt;
&lt;li&gt;upgrade execution for k3s, OS packages, and cluster components&lt;/li&gt;
&lt;li&gt;incident recovery when a node or service fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That trade can be worth it. But it is only worth it if the team actually has the time and skill to own those areas.&lt;/p&gt;

&lt;h3&gt;The hidden cost: operator hours&lt;/h3&gt;

&lt;p&gt;A rough estimate for a team that is comfortable with Linux and Kubernetes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial cluster setup: 8–16 hours&lt;/li&gt;
&lt;li&gt;Routine monthly maintenance (patches, cert rotation checks, backup verification): 2–4 hours/month&lt;/li&gt;
&lt;li&gt;Incident response per year (a bad node, a full disk, a certificate miss): 4–12 hours/year&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At a conservative rate of €50/hour for skilled operator time, the annual non-infrastructure cost is roughly €1,500–3,000. That still compares favorably with managed Kubernetes if your cluster would otherwise cost €100+/month more. But it narrows the gap significantly.&lt;/p&gt;

&lt;h2&gt;A failure case that changed how I price &quot;cheap&quot;&lt;/h2&gt;

&lt;p&gt;I think about cost differently after incidents where one operational problem cancels out a lot of monthly savings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;registry cleanup that needs blob-level validation before it is safe&lt;/li&gt;
&lt;li&gt;disk pressure caused by slow log/layer drift&lt;/li&gt;
&lt;li&gt;certificate / routing issues that depend on remembering too many special cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not arguments *against* Hetzner. They are arguments for pricing operator clarity into the platform choice.&lt;/p&gt;

&lt;h2&gt;The costs I think about beyond the invoice&lt;/h2&gt;

&lt;p&gt;Cloud cost discussions get distorted when they only compare monthly server prices. For me, the real question is broader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how many operator hours will this platform consume each month?&lt;/li&gt;
&lt;li&gt;how expensive is a networking mistake or a slow incident response?&lt;/li&gt;
&lt;li&gt;how much complexity is the team adding just to save money on compute?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes Hetzner still wins easily. Sometimes the operational overhead narrows the gap more than people expect.&lt;/p&gt;

&lt;h2&gt;Decision matrix: when Hetzner makes sense vs when it does not&lt;/h2&gt;

&lt;table style=&quot;border-collapse: collapse; margin: 1em 0; width: 100%;&quot;&gt;&lt;tr&gt;&lt;th&gt;Situation&lt;/th&gt;&lt;th&gt;Hetzner self-managed k8s&lt;/th&gt;&lt;th&gt;Managed k8s (GKE/EKS/DOKS)&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Small team, cost-sensitive, strong Linux skills&lt;/td&gt;&lt;td&gt;✅ Strong fit&lt;/td&gt;&lt;td&gt;❌ Likely overpaying&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;First-time Kubernetes operator&lt;/td&gt;&lt;td&gt;❌ High risk&lt;/td&gt;&lt;td&gt;✅ Better starting point&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Need hyperscaler-native services (Lambda, RDS, BigQuery)&lt;/td&gt;&lt;td&gt;❌ Wrong provider&lt;/td&gt;&lt;td&gt;✅ Use that provider&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Compliance requires specific certifications (FedRAMP, etc.)&lt;/td&gt;&lt;td&gt;❌ Not available&lt;/td&gt;&lt;td&gt;✅ Choose certified provider&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Moderate, stable workloads with predictable scaling&lt;/td&gt;&lt;td&gt;✅ Excellent fit&lt;/td&gt;&lt;td&gt;⚠ Fine but more expensive&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Rapid unpredictable scaling (viral traffic)&lt;/td&gt;&lt;td&gt;⚠ Manual scale-up lag&lt;/td&gt;&lt;td&gt;✅ Autoscaling built in&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Global multi-region with sub-50ms latency&lt;/td&gt;&lt;td&gt;⚠ Limited datacenters&lt;/td&gt;&lt;td&gt;✅ Global POP coverage&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Long-running stateful batch jobs with ARM64 compute&lt;/td&gt;&lt;td&gt;✅ cax series is fast&lt;/td&gt;&lt;td&gt;❌ ARM64 on AWS/GCP is limited&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;

&lt;h2&gt;When I would use Hetzner&lt;/h2&gt;

&lt;p&gt;I would seriously consider Hetzner when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the workloads are moderate in scale&lt;/li&gt;
&lt;li&gt;the team values cost efficiency and is willing to earn it with operational discipline&lt;/li&gt;
&lt;li&gt;the architecture does not depend heavily on hyperscaler-native services&lt;/li&gt;
&lt;li&gt;the operators are comfortable with Linux and networking&lt;/li&gt;
&lt;li&gt;the platform can stay relatively simple&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that situation, Hetzner can feel refreshingly direct and honest.&lt;/p&gt;

&lt;h2&gt;When I would not use Hetzner&lt;/h2&gt;

&lt;p&gt;I would choose a different path if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the organization needs extensive managed services immediately&lt;/li&gt;
&lt;li&gt;compliance or governance requirements strongly favor a different provider&lt;/li&gt;
&lt;li&gt;the team is not ready to own cluster operations&lt;/li&gt;
&lt;li&gt;the workload would be better served by a simpler platform than Kubernetes&lt;/li&gt;
&lt;li&gt;global multi-region complexity is a day-one requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the smartest infrastructure decision is not to use Kubernetes at all. A simpler deployment model is often the better business choice.&lt;/p&gt;

&lt;h2&gt;My quick decision checklist now&lt;/h2&gt;

&lt;p&gt;If I had to choose again quickly, I would ask these in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do I actually need Kubernetes, or just a reliable deployment target?&lt;/li&gt;
&lt;li&gt;Do I have someone who can explain the node-IP, firewall, backup, and upgrade story without hand-waving?&lt;/li&gt;
&lt;li&gt;Is the workload stable enough that Hetzner&#39;s lower invoice will really matter over a year?&lt;/li&gt;
&lt;li&gt;Will one avoided managed-cloud feature later cost me more in custom ops work than I saved?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;My practical decision rule&lt;/h2&gt;

&lt;p&gt;If a small team can clearly explain its node strategy, network model, restore path, and upgrade routine, Hetzner is a serious option. If those answers are fuzzy, then the cheap invoice may hide a platform the team is not actually ready to operate.&lt;/p&gt;

&lt;p&gt;That is why I do not treat Hetzner as a universal recommendation. I treat it as a strong option when the team wants control and is honest about responsibility.&lt;/p&gt;

&lt;h2&gt;The honest conclusion&lt;/h2&gt;

&lt;p&gt;Kubernetes on Hetzner is powerful when you want control, reasonable performance, and lower recurring cost. It is a poor fit if you want to avoid operational responsibility.&lt;/p&gt;

&lt;p&gt;That is the tradeoff in one sentence: you save money and keep flexibility, but you must earn reliability through disciplined configuration and maintenance.&lt;/p&gt;

&lt;p&gt;For me, that trade has been absolutely worth it. My cluster costs ~€55/month and handles real production traffic, including latency-sensitive ad-serving workloads that need sub-100ms response times. That would cost three to five times more on a managed platform with comparable performance.&lt;/p&gt;

&lt;p&gt;The key is being honest about what the platform gives you, what it does not give you, and what your team is realistically prepared to operate.&lt;/p&gt;

&lt;h2&gt;What I&#39;d do differently now&lt;/h2&gt;

&lt;p&gt;If I were writing the first cost memo again, I would include two explicit footnotes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&quot;cheap compute does not mean cheap incidents&quot;&lt;/li&gt;
&lt;li&gt;&quot;platform fit is a people/skills question as much as a pricing question&quot;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those two clarifications would have made the recommendation more honest from day one.&lt;/p&gt;

&lt;h2&gt;Series note&lt;/h2&gt;

&lt;p&gt;This closes the first six-part Kubernetes-on-Hetzner series on MisLinux. I wanted the final article to be deliberately honest rather than promotional, because platform choice only becomes useful when the tradeoffs are written down as plainly as the benefits.&lt;/p&gt;

&lt;p&gt;The series continues with two later articles covering monitoring and multi-architecture image management. The full series index is available on the &lt;a href=&quot;https://mislinux.blogspot.com/p/kubernetes-on-hetzner-series-index.html&quot; rel=&quot;nofollow noopener&quot; target=&quot;_blank&quot;&gt;Series Index page&lt;/a&gt;.&lt;/p&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1538585678430844784'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4670815028761617050/posts/default/1538585678430844784'/><link rel='alternate' type='text/html' href='http://mislinux.blogspot.com/2026/03/06-what-kubernetes-on-hetzner-costs-and.html' title='What Kubernetes on Hetzner Costs, and When I Wouldn&#39;t Use It'/><author><name>Unknown</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEgPesnj9pSG2o8AEujqRuSChPrxXgNBnurNAcNE3oR7Cv9DapTYUCH4r0HIvW4ppswbrElJHSoci_-2_JaGVLm3TfQaE-3r4RCMWnR7D9TLdlFct2KED1c60upURxy_rVFAGW8e14tMsVNM12QYo5hF8JXf1RNaFkNVethRP8vsgrtgWldqaj-YubfSQpUE=s72-c" height="72" width="72"/></entry></feed>