summaryrefslogtreecommitdiff
path: root/gemfeed
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-14 23:21:56 +0200
committerPaul Buetow <paul@buetow.org>2026-02-14 23:21:56 +0200
commitec46174748351390d95f2464c2e25889a98871f6 (patch)
tree2a7a2ff7e6fd82cb5c6e4915f53d9491f34f70d2 /gemfeed
parentafdd225c06437593c5dd9450563683c8d5dc809f (diff)
Update content for gemtext
Diffstat (limited to 'gemfeed')
-rw-r--r--gemfeed/2025-06-22-task-samurai.gmi1
-rw-r--r--gemfeed/2025-08-05-local-coding-llm-with-ollama.gmi1
-rw-r--r--gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi1
-rw-r--r--gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi84
-rw-r--r--gemfeed/atom.xml460
-rw-r--r--gemfeed/index.gmi1
6 files changed, 353 insertions, 195 deletions
diff --git a/gemfeed/2025-06-22-task-samurai.gmi b/gemfeed/2025-06-22-task-samurai.gmi
index 72479f74..2ab9e3d0 100644
--- a/gemfeed/2025-06-22-task-samurai.gmi
+++ b/gemfeed/2025-06-22-task-samurai.gmi
@@ -126,7 +126,6 @@ E-Mail your comments to `paul@nospam.buetow.org` :-)
Other related posts are:
-=> ./2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi 2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context
=> ./2025-08-05-local-coding-llm-with-ollama.gmi 2025-08-05 Local LLM for Coding with Ollama on macOS
=> ./2025-06-22-task-samurai.gmi 2025-06-22 Task Samurai: An agentic coding learning experiment (You are currently reading this)
diff --git a/gemfeed/2025-08-05-local-coding-llm-with-ollama.gmi b/gemfeed/2025-08-05-local-coding-llm-with-ollama.gmi
index 9a3a465c..39987dcf 100644
--- a/gemfeed/2025-08-05-local-coding-llm-with-ollama.gmi
+++ b/gemfeed/2025-08-05-local-coding-llm-with-ollama.gmi
@@ -421,7 +421,6 @@ E-Mail your comments to `paul@nospam.buetow.org` :-)
Other related posts are:
-=> ./2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi 2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context
=> ./2025-08-05-local-coding-llm-with-ollama.gmi 2025-08-05 Local LLM for Coding with Ollama on macOS (You are currently reading this)
=> ./2025-06-22-task-samurai.gmi 2025-06-22 Task Samurai: An agentic coding learning experiment
diff --git a/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi b/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi
index 443fbed0..0c088f04 100644
--- a/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi
+++ b/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi
@@ -213,7 +213,6 @@ Context is what the agent *knows*; commands are what the agent *does*. Both are
Other related posts:
-=> ./2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi 2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context
=> ./2026-02-14-meta-slash-commands-for-prompts-and-context.gmi 2026-02-14 Meta slash-commands to manage prompts and context for coding agents (You are currently reading this)
=> ./2026-02-02-tmux-popup-editor-for-cursor-agent-prompts.gmi 2026-02-02 A tmux popup editor for Cursor Agent CLI prompts
diff --git a/gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi b/gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi
deleted file mode 100644
index 9059b345..00000000
--- a/gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi
+++ /dev/null
@@ -1,84 +0,0 @@
-# TIL: Meta slash-commands for reusable AI prompts and context
-
-> Published at 2026-02-14T14:00:00+02:00
-
-Short post in "This week I learned" style: what I tried, why it worked, and how you can replicate it. Format: Problem → Approach → Copy/paste → Result → Gotchas. Full reference for the meta-commands lives in a separate post.
-
-=> ./2026-02-14-meta-slash-commands-for-prompts-and-context.gmi Full reference: Meta slash-commands to manage prompts and context for coding agents
-
-## TIL
-
-### Problem
-
-When I use a coding agent (Cursor Agent, Claude Code CLI, Ampcode, etc.), I repeat the same requests: "review this function," "explain this error," "add tests for this module," "format this as a blog post." Typing long prompts from scratch is tedious; ad-hoc prompts are easy to forget. I also keep pasting the same background (API conventions, project rules, infra notes) into every session so the agent doesn't guess blindly.
-
-### Approach
-
-Treat prompts and context as first-class artefacts: store them as markdown files (one per slash-command or per context) in a dotfiles repo. Use a small set of *meta* slash-commands so the agent itself creates, updates, and deletes these files from the conversation—no hand-editing. Two kinds of artefacts:
-
-* Commands — Reusable workflows (e.g. review-code, explain-error). Live in `commands/` as `.md` files.
-* Context — Reusable background (API guidelines, runbooks, personas). Live in `context/` as `.md` files; you *load* one at session start so the agent has it in mind.
-
-### Copy/paste
-
-Minimal workflow:
-
-```
-/load-context api-guidelines
-```
-
-Then ask the agent to implement a feature or fix a bug; it already has the guidelines.
-
-To turn something you just did into a reusable command:
-
-```
-/create-command review-code
-```
-
-Full command reference (all meta-commands, parameters, examples):
-
-=> ./2026-02-14-meta-slash-commands-for-prompts-and-context.gmi Meta slash-commands — full reference
-
-### Result
-
-No more retyping long prompts; same prompt library across Cursor Agent, Claude Code CLI, OpenCode, Ampcode. Context is load-on-demand per session instead of pasting walls of text. Everything is versioned in git and synced across machines.
-
-### Gotchas
-
-* Requires an agent that supports custom slash-commands (or reading prompt files from disk).
-* Context is explicit: you must run `/load-context <name>` at session start; it's not implicit like some "skills" systems.
-* A flat directory of commands can grow; if it gets unwieldy, consider grouping by skill or exposing via an MCP server later.
-
-## Before/After
-
-* Before: Retyping or pasting long prompts each time; pasting API/project context into every session; prompts scattered and inconsistent across tools.
-* After: `/load-context <name>` once per session; `/<command>` for repeatable tasks; commands and context in git, same library across agents.
-
-## Workflow recipe
-
-* Steps: (1) Create or update context/commands via meta-commands when you have something worth reusing. (2) Start a session → run `/load-context <name>`. (3) Use slash-commands or ask ad-hoc; agent has background and consistent prompts.
-* Tools: Cursor Agent (CLI), Claude Code CLI, OpenCode, Ampcode; markdown in e.g. `~/Notes/Prompts/commands/` and `~/Notes/Prompts/context/`.
-* Impact: Saves retyping and keeps prompts consistent; one-time cost is creating the first context/command with the agent.
-
-## Micro-template (quick reference)
-
-| Meta-command | Purpose | Good for |
-|--------------------|--------------------------------|-------------------------------------------|
-| /create-command | Create new slash-command | Turn current or recurring tasks into one |
-| /update-command | Edit existing slash-command | Refine after use |
-| /delete-command | Remove slash-command file | Clean up unused commands |
-| /create-context | Create new context file | Capture project/infra knowledge once |
-| /update-context | Edit existing context file | Keep context up to date |
-| /delete-context | Remove context file | Remove outdated context |
-| /load-context | Load context into conversation | Give agent background before tasks |
-
-Example usage: `/load-context api-guidelines`, `/create-command review-code`, `/update-command review-code`. Full details and examples in the main post linked above.
-
-Other related posts:
-
-=> ./2026-02-14-meta-slash-commands-for-prompts-and-context.gmi 2026-02-14 Meta slash-commands to manage prompts and context for coding agents
-=> ./2026-02-02-tmux-popup-editor-for-cursor-agent-prompts.gmi 2026-02-02 A tmux popup editor for Cursor Agent CLI prompts
-
-E-Mail your comments to `paul@nospam.buetow.org` :-)
-
-=> ../ Back to the main site
diff --git a/gemfeed/atom.xml b/gemfeed/atom.xml
index 644c9ec9..82ded3a1 100644
--- a/gemfeed/atom.xml
+++ b/gemfeed/atom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
- <updated>2026-02-14T23:11:16+02:00</updated>
+ <updated>2026-02-14T23:20:12+02:00</updated>
<title>foo.zone feed</title>
<subtitle>To be in the .zone!</subtitle>
<link href="gemini://foo.zone/gemfeed/atom.xml" rel="self" />
@@ -253,109 +253,6 @@ mage test
</content>
</entry>
<entry>
- <title>TIL: Meta slash-commands for reusable AI prompts and context</title>
- <link href="gemini://foo.zone/gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi" />
- <id>gemini://foo.zone/gemfeed/2026-02-14-til-meta-slash-commands-for-ai-workflows.gmi</id>
- <updated>2026-02-14T14:00:00+02:00</updated>
- <author>
- <name>Paul Buetow aka snonux</name>
- <email>paul@dev.buetow.org</email>
- </author>
- <summary>Short post in 'This week I learned' style: what I tried, why it worked, and how you can replicate it. Format: Problem → Approach → Copy/paste → Result → Gotchas. Full reference for the meta-commands lives in a separate post.</summary>
- <content type="xhtml">
- <div xmlns="http://www.w3.org/1999/xhtml">
- <h1 style='display: inline' id='til-meta-slash-commands-for-reusable-ai-prompts-and-context'>TIL: Meta slash-commands for reusable AI prompts and context</h1><br />
-<br />
-<span class='quote'>Published at 2026-02-14T14:00:00+02:00</span><br />
-<br />
-<span>Short post in "This week I learned" style: what I tried, why it worked, and how you can replicate it. Format: Problem → Approach → Copy/paste → Result → Gotchas. Full reference for the meta-commands lives in a separate post.</span><br />
-<br />
-<a class='textlink' href='./2026-02-14-meta-slash-commands-for-prompts-and-context.html'>Full reference: Meta slash-commands to manage prompts and context for coding agents</a><br />
-<br />
-<h2 style='display: inline' id='til'>TIL</h2><br />
-<br />
-<h3 style='display: inline' id='problem'>Problem</h3><br />
-<br />
-<span>When I use a coding agent (Cursor Agent, Claude Code CLI, Ampcode, etc.), I repeat the same requests: "review this function," "explain this error," "add tests for this module," "format this as a blog post." Typing long prompts from scratch is tedious; ad-hoc prompts are easy to forget. I also keep pasting the same background (API conventions, project rules, infra notes) into every session so the agent doesn&#39;t guess blindly.</span><br />
-<br />
-<h3 style='display: inline' id='approach'>Approach</h3><br />
-<br />
-<span>Treat prompts and context as first-class artefacts: store them as markdown files (one per slash-command or per context) in a dotfiles repo. Use a small set of *meta* slash-commands so the agent itself creates, updates, and deletes these files from the conversation—no hand-editing. Two kinds of artefacts:</span><br />
-<br />
-<ul>
-<li>Commands — Reusable workflows (e.g. review-code, explain-error). Live in <span class='inlinecode'>commands/</span> as <span class='inlinecode'>.md</span> files.</li>
-<li>Context — Reusable background (API guidelines, runbooks, personas). Live in <span class='inlinecode'>context/</span> as <span class='inlinecode'>.md</span> files; you *load* one at session start so the agent has it in mind.</li>
-</ul><br />
-<h3 style='display: inline' id='copypaste'>Copy/paste</h3><br />
-<br />
-<span>Minimal workflow:</span><br />
-<br />
-<pre>
-/load-context api-guidelines
-</pre>
-<br />
-<span>Then ask the agent to implement a feature or fix a bug; it already has the guidelines.</span><br />
-<br />
-<span>To turn something you just did into a reusable command:</span><br />
-<br />
-<pre>
-/create-command review-code
-</pre>
-<br />
-<span>Full command reference (all meta-commands, parameters, examples):</span><br />
-<br />
-<a class='textlink' href='./2026-02-14-meta-slash-commands-for-prompts-and-context.html'>Meta slash-commands — full reference</a><br />
-<br />
-<h3 style='display: inline' id='result'>Result</h3><br />
-<br />
-<span>No more retyping long prompts; same prompt library across Cursor Agent, Claude Code CLI, OpenCode, Ampcode. Context is load-on-demand per session instead of pasting walls of text. Everything is versioned in git and synced across machines.</span><br />
-<br />
-<h3 style='display: inline' id='gotchas'>Gotchas</h3><br />
-<br />
-<ul>
-<li>Requires an agent that supports custom slash-commands (or reading prompt files from disk).</li>
-<li>Context is explicit: you must run <span class='inlinecode'>/load-context &lt;name&gt;</span> at session start; it&#39;s not implicit like some "skills" systems.</li>
-<li>A flat directory of commands can grow; if it gets unwieldy, consider grouping by skill or exposing via an MCP server later.</li>
-</ul><br />
-<h2 style='display: inline' id='beforeafter'>Before/After</h2><br />
-<br />
-<ul>
-<li>Before: Retyping or pasting long prompts each time; pasting API/project context into every session; prompts scattered and inconsistent across tools.</li>
-<li>After: <span class='inlinecode'>/load-context &lt;name&gt;</span> once per session; <span class='inlinecode'>/&lt;command&gt;</span> for repeatable tasks; commands and context in git, same library across agents.</li>
-</ul><br />
-<h2 style='display: inline' id='workflow-recipe'>Workflow recipe</h2><br />
-<br />
-<ul>
-<li>Steps: (1) Create or update context/commands via meta-commands when you have something worth reusing. (2) Start a session → run <span class='inlinecode'>/load-context &lt;name&gt;</span>. (3) Use slash-commands or ask ad-hoc; agent has background and consistent prompts.</li>
-<li>Tools: Cursor Agent (CLI), Claude Code CLI, OpenCode, Ampcode; markdown in e.g. <span class='inlinecode'>~/Notes/Prompts/commands/</span> and <span class='inlinecode'>~/Notes/Prompts/context/</span>.</li>
-<li>Impact: Saves retyping and keeps prompts consistent; one-time cost is creating the first context/command with the agent.</li>
-</ul><br />
-<h2 style='display: inline' id='micro-template-quick-reference'>Micro-template (quick reference)</h2><br />
-<br />
-<span>| Meta-command | Purpose | Good for |</span><br />
-<span>|--------------------|--------------------------------|-------------------------------------------|</span><br />
-<span>| /create-command | Create new slash-command | Turn current or recurring tasks into one |</span><br />
-<span>| /update-command | Edit existing slash-command | Refine after use |</span><br />
-<span>| /delete-command | Remove slash-command file | Clean up unused commands |</span><br />
-<span>| /create-context | Create new context file | Capture project/infra knowledge once |</span><br />
-<span>| /update-context | Edit existing context file | Keep context up to date |</span><br />
-<span>| /delete-context | Remove context file | Remove outdated context |</span><br />
-<span>| /load-context | Load context into conversation | Give agent background before tasks |</span><br />
-<br />
-<span>Example usage: <span class='inlinecode'>/load-context api-guidelines</span>, <span class='inlinecode'>/create-command review-code</span>, <span class='inlinecode'>/update-command review-code</span>. Full details and examples in the main post linked above.</span><br />
-<br />
-<span>Other related posts:</span><br />
-<br />
-<a class='textlink' href='./2026-02-14-meta-slash-commands-for-prompts-and-context.html'>2026-02-14 Meta slash-commands to manage prompts and context for coding agents</a><br />
-<a class='textlink' href='./2026-02-02-tmux-popup-editor-for-cursor-agent-prompts.html'>2026-02-02 A tmux popup editor for Cursor Agent CLI prompts</a><br />
-<br />
-<span>E-Mail your comments to <span class='inlinecode'>paul@nospam.buetow.org</span> :-)</span><br />
-<br />
-<a class='textlink' href='../'>Back to the main site</a><br />
- </div>
- </content>
- </entry>
- <entry>
<title>Meta slash-commands to manage prompts and context for coding agents</title>
<link href="gemini://foo.zone/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi" />
<id>gemini://foo.zone/gemfeed/2026-02-14-meta-slash-commands-for-prompts-and-context.gmi</id>
@@ -591,7 +488,6 @@ mage test
<br />
<span>Other related posts:</span><br />
<br />
-<a class='textlink' href='./2026-02-14-til-meta-slash-commands-for-ai-workflows.html'>2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context</a><br />
<a class='textlink' href='./2026-02-14-meta-slash-commands-for-prompts-and-context.html'>2026-02-14 Meta slash-commands to manage prompts and context for coding agents (You are currently reading this)</a><br />
<a class='textlink' href='./2026-02-02-tmux-popup-editor-for-cursor-agent-prompts.html'>2026-02-02 A tmux popup editor for Cursor Agent CLI prompts</a><br />
<br />
@@ -7711,7 +7607,6 @@ content = "{CODE}"
<br />
<span>Other related posts are:</span><br />
<br />
-<a class='textlink' href='./2026-02-14-til-meta-slash-commands-for-ai-workflows.html'>2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context</a><br />
<a class='textlink' href='./2025-08-05-local-coding-llm-with-ollama.html'>2025-08-05 Local LLM for Coding with Ollama on macOS (You are currently reading this)</a><br />
<a class='textlink' href='./2025-06-22-task-samurai.html'>2025-06-22 Task Samurai: An agentic coding learning experiment</a><br />
<br />
@@ -10785,7 +10680,6 @@ http://www.gnu.org/software/src-highlite -->
<br />
<span>Other related posts are:</span><br />
<br />
-<a class='textlink' href='./2026-02-14-til-meta-slash-commands-for-ai-workflows.html'>2026-02-14 TIL: Meta slash-commands for reusable AI prompts and context</a><br />
<a class='textlink' href='./2025-08-05-local-coding-llm-with-ollama.html'>2025-08-05 Local LLM for Coding with Ollama on macOS</a><br />
<a class='textlink' href='./2025-06-22-task-samurai.html'>2025-06-22 Task Samurai: An agentic coding learning experiment (You are currently reading this)</a><br />
<br />
@@ -18418,4 +18312,356 @@ http://www.gnu.org/software/src-highlite -->
</div>
</content>
</entry>
+ <entry>
+ <title>KISS high-availability with OpenBSD</title>
+ <link href="gemini://foo.zone/gemfeed/2024-04-01-KISS-high-availability-with-OpenBSD.gmi" />
+ <id>gemini://foo.zone/gemfeed/2024-04-01-KISS-high-availability-with-OpenBSD.gmi</id>
+ <updated>2024-03-30T22:12:56+02:00</updated>
+ <author>
+ <name>Paul Buetow aka snonux</name>
+ <email>paul@dev.buetow.org</email>
+ </author>
+ <summary>I have always wanted a highly available setup for my personal websites. I could have used off-the-shelf hosting solutions or hosted my sites in an AWS S3 bucket. I have used technologies like (in unsorted and slightly unrelated order) BGP, LVS/IPVS, ldirectord, Pacemaker, STONITH, scripted VIP failover via ARP, heartbeat, heartbeat2, Corosync, keepalived, DRBD, and commercial F5 Load Balancers for high availability at work. </summary>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <h1 style='display: inline' id='kiss-high-availability-with-openbsd'>KISS high-availability with OpenBSD</h1><br />
+<br />
+<span class='quote'>Published at 2024-03-30T22:12:56+02:00</span><br />
+<br />
+<span>I have always wanted a highly available setup for my personal websites. I could have used off-the-shelf hosting solutions or hosted my sites in an AWS S3 bucket. I have used technologies like (in unsorted and slightly unrelated order) BGP, LVS/IPVS, ldirectord, Pacemaker, STONITH, scripted VIP failover via ARP, heartbeat, heartbeat2, Corosync, keepalived, DRBD, and commercial F5 Load Balancers for high availability at work. </span><br />
+<br />
+<span>But still, my personal sites were never highly available. All those technologies are great for professional use, but I was looking for something much more straightforward for my personal space - something as KISS (keep it simple and stupid) as possible.</span><br />
+<br />
+<span>It would be fine if my personal website wasn&#39;t highly available, but the geek in me wants it anyway.</span><br />
+<br />
+<span class='quote'>PS: ASCII-art below reflects an OpenBSD under-water world with all the tools available in the base system.</span><br />
+<br />
+<pre>
+Art by Michael J. Penick (mod. by Paul B.)
+ ACME-sky
+ __________
+ / nsd tower\ (
+ /____________\ (\) awk-ward
+ |:_:_:_:_:_| )) plant
+ |_:_,--.:_:| dig-bubble (\// )
+ |:_:|__|_:_| relayd-castle _ ) )) ((
+ _ |_ _ :_:| _ _ _ (_) (((( /)\`
+ | |_| |_| | _| | |_| |_| | o \\)) (( (
+ \_:_:_:_:/|_|_|_|\:_:_:_:_/ . (( ))))
+ |_,-._:_:_:_:_:_:_:_.-,_| )) ((//
+ |:|_|:_:_:,---,:_:_:|_|:| ,-. )/
+ |_:_:_:_,&#39;puffy `,_:_:_:_| _ o ,;&#39;))((
+ |:_:_:_/ _ | _ \_:_:_:| (_O (( ))
+_____|_:_:_| (o)-(o) |_:_:_|--&#39;`-. ,--. ksh under-water (((\&#39;/
+ &#39;, ;|:_:_:| -( .-. )- |:_:_:| &#39;, ; `--._\ /,---.~ goat \`))
+. ` |_:_:_| \`-&#39;/ |_:_:_|. ` . ` /()\.__( ) .,-----&#39;`-\(( sed-root
+ &#39;, ;|:_:_:| `-&#39; |:_:_:| &#39;, ; &#39;, ; `--&#39;| \ &#39;, ; &#39;, ; &#39;,&#39;)).,--
+. ` MJP ` . ` . ` . ` . httpd-soil ` . . ` . ` . ` . ` . `
+ &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ; &#39;, ;
+
+</pre>
+<br />
+<h2 style='display: inline' id='table-of-contents'>Table of Contents</h2><br />
+<br />
+<ul>
+<li><a href='#kiss-high-availability-with-openbsd'>KISS high-availability with OpenBSD</a></li>
+<li>⇢ <a href='#my-auto-failover-requirements'>My auto-failover requirements</a></li>
+<li>⇢ <a href='#my-ha-solution'>My HA solution</a></li>
+<li>⇢ ⇢ <a href='#only-openbsd-base-installation-required'>Only OpenBSD base installation required</a></li>
+<li>⇢ ⇢ <a href='#fairly-cheap-and-geo-redundant'>Fairly cheap and geo-redundant</a></li>
+<li>⇢ ⇢ <a href='#failover-time-and-split-brain'>Failover time and split-brain</a></li>
+<li>⇢ ⇢ <a href='#failover-support-for-multiple-protocols'>Failover support for multiple protocols</a></li>
+<li>⇢ ⇢ <a href='#let-s-encrypt-tls-certificates'>Let&#39;s encrypt TLS certificates</a></li>
+<li>⇢ ⇢ <a href='#monitoring'>Monitoring</a></li>
+<li>⇢ ⇢ <a href='#rex-automation'>Rex automation</a></li>
+<li>⇢ <a href='#more-ha'>More HA</a></li>
+</ul><br />
+<h2 style='display: inline' id='my-auto-failover-requirements'>My auto-failover requirements</h2><br />
+<br />
+<ul>
+<li>Be OpenBSD-based (I prefer OpenBSD because of the cleanliness and good documentation) and rely on as few external packages as possible. </li>
+<li>Don&#39;t rely on the hottest and newest tech (don&#39;t want to migrate everything to a new and fancier technology next month already!).</li>
+<li>It should be reasonably cheap. I want to avoid paying a premium for floating IPs or fancy Elastic Load Balancers.</li>
+<li>It should be geo-redundant. </li>
+<li>It&#39;s fine if my sites aren&#39;t reachable for five or ten minutes every other month. Due to their static nature, I don&#39;t care if there&#39;s a split-brain scenario where some requests reach one server and other requests reach another server.</li>
+<li>Failover should work for both HTTP/HTTPS and Gemini protocols. My self-hosted MTAs and DNS servers should also be highly available.</li>
+<li>Let&#39;s Encrypt TLS certificates should always work (before and after a failover).</li>
+<li>Have good monitoring in place so I know when a failover was performed and when something went wrong with the failover.</li>
+<li>Don&#39;t configure everything manually. The configuration should be automated and reproducible.</li>
+</ul><br />
+<h2 style='display: inline' id='my-ha-solution'>My HA solution</h2><br />
+<br />
+<h3 style='display: inline' id='only-openbsd-base-installation-required'>Only OpenBSD base installation required</h3><br />
+<br />
+<span>My HA solution for Web and Gemini is based on DNS (OpenBSD&#39;s <span class='inlinecode'>nsd</span>) and a simple shell script (OpenBSD&#39;s <span class='inlinecode'>ksh</span> and some little <span class='inlinecode'>sed</span> and <span class='inlinecode'>awk</span> and <span class='inlinecode'>grep</span>). All software used here is part of the OpenBSD base system and no external package needs to be installed - OpenBSD is a complete operating system.</span><br />
+<br />
+<a class='textlink' href='https://man.OpenBSD.org/nsd.8'>https://man.OpenBSD.org/nsd.8</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/ksh'>https://man.OpenBSD.org/ksh</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/awk'>https://man.OpenBSD.org/awk</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/sed'>https://man.OpenBSD.org/sed</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/dig'>https://man.OpenBSD.org/dig</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/ftp'>https://man.OpenBSD.org/ftp</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/cron'>https://man.OpenBSD.org/cron</a><br />
+<br />
+<span>I also used the <span class='inlinecode'>dig</span> (for DNS checks) and <span class='inlinecode'>ftp</span> (for HTTP/HTTPS checks) programs. </span><br />
+<br />
+<span>The DNS failover is performed automatically between the two OpenBSD VMs involved (my setup doesn&#39;t require any quorum for a failover, so there isn&#39;t a need for a 3rd VM). The <span class='inlinecode'>ksh</span> script, executed once per minute via CRON (on both VMs), performs a health check to determine whether the current master node is available. If the current master isn&#39;t available (no HTTP response as expected), a failover is performed to the standby VM: </span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre><i><font color="silver">#!/bin/ksh</font></i>
+
+ZONES_DIR=/var/nsd/zones/master/
+DEFAULT_MASTER=fishfinger.buetow.org
+DEFAULT_STANDBY=blowfish.buetow.org
+
+determine_master_and_standby () {
+ <b><u><font color="#000000">local</font></u></b> master=$DEFAULT_MASTER
+ <b><u><font color="#000000">local</font></u></b> standby=$DEFAULT_STANDBY
+
+ .
+ .
+ .
+
+ <b><u><font color="#000000">local</font></u></b> -i health_ok=<font color="#000000">1</font>
+ <b><u><font color="#000000">if</font></u></b> ! ftp -<font color="#000000">4</font> -o - https://$master/index.txt | grep -q <font color="#808080">"Welcome to $master"</font>; <b><u><font color="#000000">then</font></u></b>
+ echo <font color="#808080">"https://$master/index.txt IPv4 health check failed"</font>
+ health_ok=<font color="#000000">0</font>
+ <b><u><font color="#000000">elif</font></u></b> ! ftp -<font color="#000000">6</font> -o - https://$master/index.txt | grep -q <font color="#808080">"Welcome to $master"</font>; <b><u><font color="#000000">then</font></u></b>
+ echo <font color="#808080">"https://$master/index.txt IPv6 health check failed"</font>
+ health_ok=<font color="#000000">0</font>
+ <b><u><font color="#000000">fi</font></u></b>
+ <b><u><font color="#000000">if</font></u></b> [ $health_ok -eq <font color="#000000">0</font> ]; <b><u><font color="#000000">then</font></u></b>
+ <b><u><font color="#000000">local</font></u></b> tmp=$master
+ master=$standby
+ standby=$tmp
+ <b><u><font color="#000000">fi</font></u></b>
+
+ .
+ .
+ .
+}
+</pre>
+<br />
+<span>The failover scripts looks for the <span class='inlinecode'> ; Enable failover</span> string in the DNS zone files and swaps the <span class='inlinecode'>A</span> and <span class='inlinecode'>AAAA</span> records of the DNS entries accordingly:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>fishfinger$ grep failover /var/nsd/zones/master/foo.zone.zone
+ <font color="#000000">300</font> IN A <font color="#000000">46.23</font>.<font color="#000000">94.99</font> ; Enable failover
+ <font color="#000000">300</font> IN AAAA 2a03:<font color="#000000">6000</font>:6f67:<font color="#000000">624</font>::<font color="#000000">99</font> ; Enable failover
+www <font color="#000000">300</font> IN A <font color="#000000">46.23</font>.<font color="#000000">94.99</font> ; Enable failover
+www <font color="#000000">300</font> IN AAAA 2a03:<font color="#000000">6000</font>:6f67:<font color="#000000">624</font>::<font color="#000000">99</font> ; Enable failover
+standby <font color="#000000">300</font> IN A <font color="#000000">23.88</font>.<font color="#000000">35.144</font> ; Enable failover
+standby <font color="#000000">300</font> IN AAAA 2a01:4f8:c17:20f1::<font color="#000000">42</font> ; Enable failover
+</pre>
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>transform () {
+ sed -E <font color="#808080">'</font>
+<font color="#808080"> /IN A .*; Enable failover/ {</font>
+<font color="#808080"> /^standby/! {</font>
+<font color="#808080"> s/^(.*) 300 IN A (.*) ; (.*)/</font>\1<font color="#808080"> 300 IN A '</font>$(cat /var/nsd/run/master_a)<font color="#808080">' ; </font>\3<font color="#808080">/;</font>
+<font color="#808080"> }</font>
+<font color="#808080"> /^standby/ {</font>
+<font color="#808080"> s/^(.*) 300 IN A (.*) ; (.*)/</font>\1<font color="#808080"> 300 IN A '</font>$(cat /var/nsd/run/standby_a)<font color="#808080">' ; </font>\3<font color="#808080">/;</font>
+<font color="#808080"> }</font>
+<font color="#808080"> }</font>
+<font color="#808080"> /IN AAAA .*; Enable failover/ {</font>
+<font color="#808080"> /^standby/! {</font>
+<font color="#808080"> s/^(.*) 300 IN AAAA (.*) ; (.*)/</font>\1<font color="#808080"> 300 IN AAAA '</font>$(cat /var/nsd/run/master_aaaa)<font color="#808080">' ; </font>\3<font color="#808080">/;</font>
+<font color="#808080"> }</font>
+<font color="#808080"> /^standby/ {</font>
+<font color="#808080"> s/^(.*) 300 IN AAAA (.*) ; (.*)/</font>\1<font color="#808080"> 300 IN AAAA '</font>$(cat /var/nsd/run/standby_aaaa)<font color="#808080">' ; </font>\3<font color="#808080">/;</font>
+<font color="#808080"> }</font>
+<font color="#808080"> }</font>
+<font color="#808080"> / ; serial/ {</font>
+<font color="#808080"> s/^( +) ([0-9]+) .*; (.*)/</font>\1<font color="#808080"> '</font>$(date +%s)<font color="#808080">' ; </font>\3<font color="#808080">/;</font>
+<font color="#808080"> }</font>
+<font color="#808080"> '</font>
+}
+</pre>
+<br />
+<span>After the failover, the script reloads <span class='inlinecode'>nsd</span> and performs a sanity check to see if DNS still works. If not, a rollback will be performed:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre><i><font color="silver">#! Race condition !#</font></i>
+
+<b><u><font color="#000000">if</font></u></b> [ -f $zone_file.bak ]; <b><u><font color="#000000">then</font></u></b>
+ mv $zone_file.bak $zone_file
+<b><u><font color="#000000">fi</font></u></b>
+
+cat $zone_file | transform &gt; $zone_file.new.tmp
+
+grep -v <font color="#808080">' ; serial'</font> $zone_file.new.tmp &gt; $zone_file.new.noserial.tmp
+grep -v <font color="#808080">' ; serial'</font> $zone_file &gt; $zone_file.old.noserial.tmp
+
+echo <font color="#808080">"Has zone $zone_file changed?"</font>
+<b><u><font color="#000000">if</font></u></b> diff -u $zone_file.old.noserial.tmp $zone_file.new.noserial.tmp; <b><u><font color="#000000">then</font></u></b>
+ echo <font color="#808080">"The zone $zone_file hasn't changed"</font>
+ rm $zone_file.*.tmp
+ <b><u><font color="#000000">return</font></u></b> <font color="#000000">0</font>
+<b><u><font color="#000000">fi</font></u></b>
+
+cp $zone_file $zone_file.bak
+mv $zone_file.new.tmp $zone_file
+rm $zone_file.*.tmp
+echo <font color="#808080">"Reloading nsd"</font>
+nsd-control reload
+
+<b><u><font color="#000000">if</font></u></b> ! zone_is_ok $zone; <b><u><font color="#000000">then</font></u></b>
+ echo <font color="#808080">"Rolling back $zone_file changes"</font>
+ cp $zone_file $zone_file.invalid
+ mv $zone_file.bak $zone_file
+ echo <font color="#808080">"Reloading nsd"</font>
+ nsd-control reload
+ zone_is_ok $zone
+ <b><u><font color="#000000">return</font></u></b> <font color="#000000">3</font>
+<b><u><font color="#000000">fi</font></u></b>
+
+<b><u><font color="#000000">for</font></u></b> cleanup <b><u><font color="#000000">in</font></u></b> invalid bak; <b><u><font color="#000000">do</font></u></b>
+ <b><u><font color="#000000">if</font></u></b> [ -f $zone_file.$cleanup ]; <b><u><font color="#000000">then</font></u></b>
+ rm $zone_file.$cleanup
+ <b><u><font color="#000000">fi</font></u></b>
+<b><u><font color="#000000">done</font></u></b>
+
+echo <font color="#808080">"Failover of zone $zone to $MASTER completed"</font>
+<b><u><font color="#000000">return</font></u></b> <font color="#000000">1</font>
+</pre>
+<br />
+<span>A non-zero return code (here, 3 when a rollback and 1 when a DNS failover was performed) will cause CRON to send an E-Mail with the whole script output.</span><br />
+<br />
+<span>The authorative nameserver for my domains runs on both VMs, and both are configured to be a "master" DNS server so that they have their own individual zone files, which can be changed independently. Otherwise, my setup wouldn&#39;t work. The side effect is that under a split-brain scenario (both VMs cannot see each other), both would promote themselves to master via their local DNS entries. More about that later, but that&#39;s fine in my use case.</span><br />
+<br />
+<span>Check out the whole script here:</span><br />
+<br />
+<a class='textlink' href='https://codeberg.org/snonux/conf/src/branch/master/frontends/scripts/dns-failover.ksh'>dns-failover.ksh</a><br />
+<br />
+<h3 style='display: inline' id='fairly-cheap-and-geo-redundant'>Fairly cheap and geo-redundant</h3><br />
+<br />
+<span>I am renting two small OpenBSD VMs: One at OpenBSD Amsterdam and the other at Hetzner Cloud. So, both VMs are hosted at another provider, in different IP subnets, and in different countries (the Netherlands and Germany).</span><br />
+<br />
+<a class='textlink' href='https://OpenBSD.Amsterdam'>https://OpenBSD.Amsterdam</a><br />
+<a class='textlink' href='https://www.Hetzner.cloud'>https://www.Hetzner.cloud</a><br />
+<br />
+<span>I only have a little traffic on my sites. I could always upload the static content to AWS S3 if I suddenly had to. But this will never be required.</span><br />
+<br />
+<span>A DNS-based failover is cheap, as there isn&#39;t any BGP or fancy load balancer to pay for. Small VMs also cost less than millions.</span><br />
+<br />
+<h3 style='display: inline' id='failover-time-and-split-brain'>Failover time and split-brain</h3><br />
+<br />
+<span>A DNS failover doesn&#39;t happen immediately. I&#39;ve configured a DNS TTL of <span class='inlinecode'>300</span> seconds, and the failover script checks once per minute whether to perform a failover or not. So, in total, a failover can take six minutes (not including other DNS caching servers somewhere in the interweb, but that&#39;s fine - eventually, all requests will resolve to the new master after a failover).</span><br />
+<br />
+<span>A split-brain scenario between the old master and the new master might happen. That&#39;s OK, as my sites are static, and there&#39;s no database to synchronise other than HTML, CSS, and images when the site is updated.</span><br />
+<br />
+<h3 style='display: inline' id='failover-support-for-multiple-protocols'>Failover support for multiple protocols</h3><br />
+<br />
+<span>With the DNS failover, HTTP, HTTPS, and Gemini protocols are failovered. This works because all domain virtual hosts are configured on either VM&#39;s <span class='inlinecode'>httpd</span> (OpenBSD&#39;s HTTP server) and <span class='inlinecode'>relayd</span> (it&#39;s also part of OpenBSD and I use it to TLS offload the Gemini protocol). So, both VMs accept requests for all the hosts. It&#39;s just a matter of the DNS entries, which VM receives the requests.</span><br />
+<br />
+<a class='textlink' href='https://man.OpenBSD.org/httpd.8'>https://man.OpenBSD.org/httpd.8</a><br />
+<a class='textlink' href='https://man.OpenBSD.org/relayd.8'>https://man.OpenBSD.org/relayd.8</a><br />
+<br />
+<span>For example, the master is responsible for the <span class='inlinecode'>https://www.foo.zone</span> and <span class='inlinecode'>https://foo.zone</span> hosts, whereas the standby can be reached via <span class='inlinecode'>https://standby.foo.zone</span> (port 80 for plain HTTP works as well). The same principle is followed with all the other hosts, e.g. <span class='inlinecode'>irregular.ninja</span>, <span class='inlinecode'>paul.buetow.org</span> and so on. The same applies to my Gemini capsules for <span class='inlinecode'>gemini://foo.zone</span>, <span class='inlinecode'>gemini://standby.foo.zone</span>, <span class='inlinecode'>gemini://paul.buetow.org</span> and <sp