<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on fhoekstra</title><link>https://fhoekstra.eu/posts/</link><description>Recent content in Posts on fhoekstra</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>&lt;a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener"&gt;CC BY-NC 4.0&lt;/a&gt;</copyright><lastBuildDate>Wed, 25 Feb 2026 09:17:05 +0100</lastBuildDate><atom:link href="https://fhoekstra.eu/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Ingress Nginx to Envoy Gateway Migration</title><link>https://fhoekstra.eu/posts/ingress-nginx-to-envoy-gateway-migration/</link><pubDate>Wed, 25 Feb 2026 09:17:05 +0100</pubDate><guid>https://fhoekstra.eu/posts/ingress-nginx-to-envoy-gateway-migration/</guid><description>&lt;p&gt;Last november, the Ingress NGINX &lt;a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/"&gt;retirement was announced&lt;/a&gt; for March 2026.&lt;/p&gt;
&lt;p&gt;Are you still using Ingress NGINX? Do you want a complete example of how to migrate to the future-proof Gateway API? Wondering how to migrate forward auth?&lt;/p&gt;
&lt;p&gt;I did the migration on my homelab only this month (I know, I&amp;rsquo;m late), but I made sure to have a clean string of commits with no other changes or dependency bumps between them. I wanted to make it easy to view the full extent of the migration from nginx-ingress to Gateway API with everything it entails: forward auth, Anubis bot protection, etc.&lt;/p&gt;</description><content type="html"><![CDATA[<p>Last november, the Ingress NGINX <a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/">retirement was announced</a> for March 2026.</p>
<p>Are you still using Ingress NGINX? Do you want a complete example of how to migrate to the future-proof Gateway API? Wondering how to migrate forward auth?</p>
<p>I did the migration on my homelab only this month (I know, I&rsquo;m late), but I made sure to have a clean string of commits with no other changes or dependency bumps between them. I wanted to make it easy to view the full extent of the migration from nginx-ingress to Gateway API with everything it entails: forward auth, Anubis bot protection, etc.</p>
<p>I&rsquo;ve tagged them for your convenience. This should be especially useful if your networking setup is from <a href="https://github.com/onedr0p/cluster-template">cluster-template</a> 1 to 2 years ago, or if you&rsquo;re using forward auth with <a href="https://www.authelia.com/">Authelia</a> or <a href="https://goauthentik.io/">Authentik</a>, or if you have some public endpoints protected by <a href="https://anubis.techaro.lol/">the Anubis bot blocker</a>.</p>
<ul>
<li><code>git clone https://github.com/fhoekstra/home-ops</code></li>
<li>Or compare the <a href="https://github.com/fhoekstra/home-ops/tags">tags</a> right here in your browser by choosing <code>Compare</code> on one of the following pages:
<ul>
<li><a href="https://github.com/fhoekstra/home-ops/releases/tag/before-gateway-migration">before-gateway-migration</a></li>
<li><a href="https://github.com/fhoekstra/home-ops/releases/tag/first-app-working-with-gateway">first-app-working-with-gateway</a></li>
<li><a href="https://github.com/fhoekstra/home-ops/releases/tag/after-gateway-migration">after-gateway-migration</a></li>
</ul>
</li>
</ul>
]]></content></item><item><title>Disable Internal Laptop Keyboard When External Keyboard Is Plugged In On Linux</title><link>https://fhoekstra.eu/posts/linux-disable-internal-laptop-keyboard-when-external-keyboard-plugged-in/</link><pubDate>Thu, 01 Jan 2026 11:25:24 +0100</pubDate><guid>https://fhoekstra.eu/posts/linux-disable-internal-laptop-keyboard-when-external-keyboard-plugged-in/</guid><description>&lt;img src="./split_keyboard_on_top_of_laptop_on_lap_in_train.jpg" alt="Split keyboard on top of a laptop on a lap, sat in a train, with various bags and suitcases in the background" class="center" style="border-radius: 8px;" /&gt;
&lt;p&gt;Do you also find yourself wanting to use your custom keyboard while travelling with your Linux laptop? Without triggering keypresses through the built-in keyboard underneath of course!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll first show you how to disable the built-in laptop keyboard with a bash script. And then automatically disable the laptop keyboard when, and only when, your custom keyboard is plugged in, and enable it again when your custom keyboard is unplugged.&lt;/p&gt;</description><content type="html"><![CDATA[
    <img src="./split_keyboard_on_top_of_laptop_on_lap_in_train.jpg"  alt="Split keyboard on top of a laptop on a lap, sat in a train, with various bags and suitcases in the background"  class="center"  style="border-radius: 8px;"  />


<p>Do you also find yourself wanting to use your custom keyboard while travelling with your Linux laptop? Without triggering keypresses through the built-in keyboard underneath of course!</p>
<p>I&rsquo;ll first show you how to disable the built-in laptop keyboard with a bash script. And then automatically disable the laptop keyboard when, and only when, your custom keyboard is plugged in, and enable it again when your custom keyboard is unplugged.</p>
<h2 id="how-to-disable-the-built-in-keyboard">How to disable the built-in keyboard</h2>
<p>You can disable the keyboard dynamically by writing <code>1</code> to the inhibited property in <code>sys/devices/.../input/input3</code>. Enabling is a matter of writing <code>0</code> to the same path.</p>
<p>First we need to find the precise SysFs path of the internal keyboard:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cat /proc/bus/input/devices | less
</span></span></code></pre></div><p>If necessary, type <code>/</code> then <code>keyboard</code> to search the output.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[...]
</span></span><span style="display:flex;"><span>N: Name=&#34;AT Translated Set 2 keyboard&#34;
</span></span><span style="display:flex;"><span>P: Phys=isa0060/serio0/input0
</span></span><span style="display:flex;"><span>S: Sysfs=/devices/platform/i8042/serio0/input/input3
</span></span><span style="display:flex;"><span>[...]
</span></span></code></pre></div><p>Fill in the SYSFS_PATH in the following script, which I save to <code>/home/freek/Scripts/laptop-kb.sh</code> (and make it executable with <code>chmod +x &lt;filepath&gt;</code>):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>SYSFS_PATH<span style="color:#f92672">=</span>/devices/platform/i8042/serio0/input/input3
</span></span><span style="display:flex;"><span>DEV_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/sys/</span><span style="color:#e6db74">${</span>SYSFS_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">/inhibited&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span> in
</span></span><span style="display:flex;"><span>    disable<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#ae81ff">1</span> | sudo tee <span style="color:#e6db74">&#34;</span>$DEV_PATH<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        ;;
</span></span><span style="display:flex;"><span>    enable<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#ae81ff">0</span> | sudo tee <span style="color:#e6db74">&#34;</span>$DEV_PATH<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        ;;
</span></span><span style="display:flex;"><span>    toggle<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>        cur<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat <span style="color:#e6db74">&#34;</span>$DEV_PATH<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>        new<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$cur<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> $new<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>; <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>        echo $new | sudo tee <span style="color:#e6db74">&#34;</span>$DEV_PATH<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        ;;
</span></span><span style="display:flex;"><span>    *<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;Usage: </span>$0<span style="color:#e6db74"> {enable|disable|toggle}&#34;</span> &gt;&amp;<span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>        exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        ;;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span>
</span></span></code></pre></div><h2 id="trigger-on-plug--unplug-external-keyboard">Trigger on plug / unplug external keyboard</h2>
<p>To only disable it when a specific external keyboard is plugged in, write a udev rule that will trigger different actions on plug and unplug of the external keyboard.
<code>udev</code> is part of <code>systemd</code> and included in most modern Linux distributions, including Arch Linux, by default. Even distributions that ship without systemd, such as Void Linux, often have a compatible implementation that also accepts udev rules, such as <code>eudev</code>.</p>
<p>First find the external keyboard&rsquo;s PRODUCT id by watching the output of this command:</p>
<p><code>sudo udevadm monitor --kernel --property --subsystem-match=usb</code></p>
<p>While plugging and unplugging the external keyboard.</p>
<p>For my keyboard, it is: <code>PRODUCT=a8f8/1836/200</code></p>
<p>Then write a udev rules file with the following rules. Replace your PRODUCT id and script path and save this to <code>/etc/udev/rules.d/99-disable-internal-keyboard.rules</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>SUBSYSTEM<span style="color:#f92672">==</span><span style="color:#e6db74">&#34;usb&#34;</span>, ACTION<span style="color:#f92672">==</span><span style="color:#e6db74">&#34;add&#34;</span>, ENV<span style="color:#f92672">{</span>DEVTYPE<span style="color:#f92672">}==</span><span style="color:#e6db74">&#34;usb_device&#34;</span>, ENV<span style="color:#f92672">{</span>PRODUCT<span style="color:#f92672">}==</span><span style="color:#e6db74">&#34;a8f8/1836/200&#34;</span>, RUN<span style="color:#f92672">+=</span><span style="color:#e6db74">&#34;/home/freek/Scripts/laptop-kb.sh disable&#34;</span>
</span></span><span style="display:flex;"><span>SUBSYSTEM<span style="color:#f92672">==</span><span style="color:#e6db74">&#34;usb&#34;</span>, ACTION<span style="color:#f92672">==</span><span style="color:#e6db74">&#34;remove&#34;</span>, ENV<span style="color:#f92672">{</span>DEVTYPE<span style="color:#f92672">}==</span><span style="color:#e6db74">&#34;usb_device&#34;</span>, ENV<span style="color:#f92672">{</span>PRODUCT<span style="color:#f92672">}==</span><span style="color:#e6db74">&#34;a8f8/1836/200&#34;</span>, RUN<span style="color:#f92672">+=</span><span style="color:#e6db74">&#34;/home/freek/Scripts/laptop-kb.sh enable&#34;</span>
</span></span></code></pre></div><p>Then reload the udev rules by rebooting or run: <code>udevadm control --reload-rules &amp;&amp; udevadm trigger</code></p>
<p>Have fun working on your Linux laptop anywhere with your custom keyboard!</p>
<hr>
<p>Thanks to <a href="https://koen.vervloesem.eu/blog/automatically-disable-your-internal-webcam-on-linux/">Koen Vervloesem</a> for figuring this stuff out for webcams.</p>
]]></content></item><item><title>CKS Tips</title><link>https://fhoekstra.eu/posts/cks-tips/</link><pubDate>Tue, 09 Dec 2025 21:32:27 +0100</pubDate><guid>https://fhoekstra.eu/posts/cks-tips/</guid><description>&lt;h1 id="cks-tips-and-takeaways"&gt;CKS tips and takeaways&lt;/h1&gt;
&lt;p&gt;Yay! I got my &amp;lsquo;CKS - Certified Kubernetes Security Specialist&amp;rsquo; certification.&lt;/p&gt;
&lt;p&gt;It was quite a ride, as I had almost forgotten about it. You see, I had 2 goals this year: &lt;a href="https://github.com/fhoekstra/home-ops"&gt;the GitOps homelab&lt;/a&gt; and CKS. I have done and learned a lot in the homelab, so much so that I had neglected the CKS a bit. I was under the impression that I had some time left when I was reminded by The Linux Foundation that I only had 4 weeks left to plan the exam (and potential retake). After those 4 weeks, the money my employer generously paid for me to get certified would be down the drain. So I applied myself and studied as hard as I could.&lt;/p&gt;</description><content type="html"><![CDATA[<h1 id="cks-tips-and-takeaways">CKS tips and takeaways</h1>
<p>Yay! I got my &lsquo;CKS - Certified Kubernetes Security Specialist&rsquo; certification.</p>
<p>It was quite a ride, as I had almost forgotten about it. You see, I had 2 goals this year: <a href="https://github.com/fhoekstra/home-ops">the GitOps homelab</a> and CKS. I have done and learned a lot in the homelab, so much so that I had neglected the CKS a bit. I was under the impression that I had some time left when I was reminded by The Linux Foundation that I only had 4 weeks left to plan the exam (and potential retake). After those 4 weeks, the money my employer generously paid for me to get certified would be down the drain. So I applied myself and studied as hard as I could.</p>
<p>I knew CKA had taken me some practice, not because it is so hard, but because you need to be fast to get through all the assignments. CKA, CKS and CKAD are hands on exams where you get a terminal, <code>vim</code> <code>kubectl</code> (and the <code>k</code> alias luckily), and a Firefox browser that can only browse the official documentation pages.</p>
<p>In order to get through all the assignments in the allotted time, you have to know your way around the documentation very well.</p>
<h2 id="practice-practice-practice">Practice, practice, practice</h2>
<p>For CKA, I prepared using the Linux Foundation course. I did some of the labs multiple times to ensure I could do an &ldquo;open&rdquo; assignment unguided using just the documentation. Being able to do the labs where every step is instructed in detail is not enough.</p>
<p>For CKS, I heard from friends that there is a serious Linux sysadmin component to it, that happens on the Linux (Debian on the exam) nodes. As a former developer turned Kubernetes Engineer, I felt that the Linux Foundation course labs did not prepare me sufficiently for that. I am not affiliated with Kodekloud, but their course is great, and the labs being prepared for you in the cloud is a huge productivity booster. So I highly recommend taking a Kodekloud subscription for a month to do their labs.</p>
<p>And remember to stay focused in your practice: are you just following instructions, or can you find these instructions and example configs to edit in the official documentation? If what you are doing in a lab is not obvious to you, try doing it again using just the official documentation.</p>
<h2 id="embrace-vim">Embrace Vim</h2>
<p>I spent the last 2 years (since the CKA exam) using (neo)vim as my main IDE, but if you don&rsquo;t yet, spend some time getting used to vim.
It really helps with speed on the exam to be able to indent the next 5 lines at once (<code>^</code>, <code>Ctrl+V</code>, <code>5j</code>, space space, <code>Esc</code>), move or replace an entire line(<code>dd</code>, then <code>P</code> to paste above current line, <code>p</code> to paste below, <code>cc</code> to replace, <code>Shift+V</code> to select multiple lines, then <code>d</code> for delete or <code>c</code> for change), or change text within <code>&quot;&quot;</code> (simply <code>ci&quot;</code>, <em>c</em>hange <em>i</em>nside <code>&quot;</code>).</p>
<p>You will never get this fast with <code>nano</code>. Embrace vim, leave the mouse alone as much as possible. Your shoulders, your wrists and <a href="https://neovim.io/doc/user/uganda.html">the children in Uganda</a> will thank you.</p>
<h2 id="practice-exams">Practice exams</h2>
<p>The Linux Foundation gives you 2 Killer.sh practice exams, and kodekloud also provides 3 practice exams. I found the kodekloud exams had a lot of overlap (I did 2 and felt like most of it was the exact same assignment), and their environment is more helpful/easier: zsh with better autocomplete, whereas the real exam has a bare bash shell where autocomplete only happens with some delay after you press tab, not with greyed out text appearing as you&rsquo;re typing (like zsh).</p>
<p>So keep that in mind and use your killer.sh practice exams when you feel you&rsquo;ve mastered the labs!</p>
<h2 id="aliases-and-shortcuts">aliases and shortcuts</h2>
<p><em>Don&rsquo;t</em>. For CKA 2 years ago, I had trained myself to write out some 3-letter aliases of the <a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/kubectl">oh-my-zsh kubectl plugin</a>, as well as <code>kubectl config set-context --current --namespace</code> and <code>--dry-run -oyaml</code> as a var.
Now, there was a different node you ssh&rsquo;ed into for every assignment, so those aliases wouldn&rsquo;t stick around anyway, and the assignments and files were set up such that you usually didn&rsquo;t need to <code>k get deploy name -o yaml</code>. I still did often, purely out of habit, before noticing the yaml was already on the filesystem.</p>
<h2 id="yq--jq-chops">yq / jq chops</h2>
<p>Like vim, these skills are generally good to have, not just for the exam. I spent the night before the exam practicing them a bit, because I noticed I was scrolling through YAML and searching through json in <code>less</code> (with <code>/</code>, <code>n</code>, <code>N</code>) for things that could have been simple queries. In my quest to save any minute I could off of the time I need, I decided to practice my jq/yq query writing.</p>
<p>In the end, <em>none of the assignments in the actual exam required these skills</em>, but I am still grateful to finally understand when I need to do <code>.someArraywithbrackets[] | select .key == &quot;value&quot;</code> and when I need to do <code>.someArraywithoutbrackets | group_by(.key)</code>.</p>
<h2 id="enjoy-the-process">Enjoy the process!</h2>
<p>If you follow these tips, you should have no trouble learning all the required tools and skills for CKS, or any other Kubernetes exam, such as CKA or CKAD. Then the last tip is to enjoy it!</p>
<p>I find these hands-on exams very satisfying to do: they give me a feeling of mastery and confidence that no multiple choice or pen and paper exam has ever given me. Engineers and tinkerers who love doing and fixing things should really try these hands-on exams. Coming out of the exam, I felt like a Kubernetes God: &ldquo;I can fix any cluster, write any policy manifest and find any misbehaving container now!&rdquo;
It sounds stupid, but if you know, you know. It&rsquo;s a really good feeling and one of the best ways to demonstrate your technical ability.</p>
]]></content></item><item><title>Changelog From Conventional Commits: Git-Cliff</title><link>https://fhoekstra.eu/posts/changelog-from-conventional-commits-git-cliff/</link><pubDate>Sat, 08 Nov 2025 10:56:04 +0100</pubDate><guid>https://fhoekstra.eu/posts/changelog-from-conventional-commits-git-cliff/</guid><description>&lt;h2 id="the-changelog-in-platform-engineering"&gt;The changelog in platform engineering&lt;/h2&gt;
&lt;p&gt;At my current job, we maintain a package that is used by developers in their own codebases to use a database service on the platform. Like any package or artifact that is released to then be used by application developers, this requires a clear release with a &lt;a href="https://semver.org/"&gt;semantic version&lt;/a&gt; that communicates what users can expect from it: Does it contain only fixes (patch), or are there new features in the release (minor)? Most importantly, do users (potentially) need to change &lt;em&gt;how&lt;/em&gt; they use the package when upgrading (major)?&lt;/p&gt;</description><content type="html"><![CDATA[<h2 id="the-changelog-in-platform-engineering">The changelog in platform engineering</h2>
<p>At my current job, we maintain a package that is used by developers in their own codebases to use a database service on the platform. Like any package or artifact that is released to then be used by application developers, this requires a clear release with a <a href="https://semver.org/">semantic version</a> that communicates what users can expect from it: Does it contain only fixes (patch), or are there new features in the release (minor)? Most importantly, do users (potentially) need to change <em>how</em> they use the package when upgrading (major)?</p>
<p>Ideally, the version communicates the category of the release: patch (fixes only), minor (new features), or major (breaking changes), and the details are found in a changelog. Without a changelog, your users have a hard time figuring out what to expect from a new release.</p>
<h2 id="cant-i-just-write-a-changelog-myself">Can&rsquo;t I just write a changelog myself?</h2>
<p>If your users are normal end users, you can have the product and/or UX people write a changelog that is aimed at the end-user experience. But when your users are developers who integrate your package with their own application, completeness is key! You cannot know <a href="https://xkcd.com/1172/">all the ways</a> and context in which developers use your software, so it is important to be careful and complete and mark changes that are potentially breaking as such. If you forget to mention a change, that could waste a lot of developer time on debugging.</p>
<p>The reason why you could forget relevant changes, is because the changelog is fundamentally a <em>duplication</em>. In a changelog, you describe the changes to the software since the previous release. But as a platform engineer, you should already have this information somewhere else: in the git log!</p>
<p>If you follow <a href="https://conventionalcommits.org/">conventional commits</a>, your commits already contain this information:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>feat(api)!: Make retentionPeriod a required property on orderDetails
</span></span><span style="display:flex;"><span>fix: OOM on submission of large orderList
</span></span><span style="display:flex;"><span>chore: Bump Python to 3.14.0
</span></span></code></pre></div><p>Thus, I needed to generate a changelog (and a next version number) based on the conventional commits in the git log between the last release and now. Seems like it should be a common problem.</p>
<h2 id="what-if-youre-not-on-github">What if you&rsquo;re not on Github?</h2>
<p>To my surprise, I saw that the most commonly used tool for this problem does not use plain Git: <a href="https://github.com/googleapis/release-please">release-please</a> is built around the Github API. We use a less commonly used forge, so this did not work for us. The alternative <a href="https://github.com/release-it/release-it">release-it</a> has bindings for the GitLab and GitLab APIs, but also has a way to generate a changelog from pure Git.
Since we do not directly depend on any NodeJS projects in the team I work, I looked a bit further for a self-contained binary that really only does changelog and version generation from git.</p>
<h2 id="git-cliff">Git Cliff</h2>
<p>Git Cliff is precisely what I was looking for:</p>
<ul>
<li>only does changelog and semver generation from git</li>
<li>self-contained binary</li>
<li>sensible defaults, works out of the box</li>
<li>very customizable</li>
<li><a href="https://git-cliff.org/docs/">great documentation</a></li>
<li>written in Rust and available from many repositories</li>
</ul>
<p>To generate a configuration file with the defaults and a lot of helpful comments, run <code>git-cliff --init</code>.</p>
<p>The <code>cliff.toml</code> default configuration has a section that looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">commit_parsers</span> = [
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">message</span> = <span style="color:#e6db74">&#34;^feat&#34;</span>, <span style="color:#a6e22e">group</span> = <span style="color:#e6db74">&#34;&lt;!-- 0 --&gt;🚀 Features&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">message</span> = <span style="color:#e6db74">&#34;^fix&#34;</span>, <span style="color:#a6e22e">group</span> = <span style="color:#e6db74">&#34;&lt;!-- 1 --&gt;🐛 Bug Fixes&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">message</span> = <span style="color:#e6db74">&#34;^doc&#34;</span>, <span style="color:#a6e22e">group</span> = <span style="color:#e6db74">&#34;&lt;!-- 3 --&gt;📚 Documentation&#34;</span> },
</span></span><span style="display:flex;"><span>    ...
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>You can add or remove parsers easily: the message field is the regex pattern that is matched to the commit message, and the number in the group determines the order of the sections in the generated changelog.</p>
<p>Some useful commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span># Generate the changelog file by running:
</span></span><span style="display:flex;"><span>git-cliff -o CHANGELOG.md
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># Get the last released (tagged) version:
</span></span><span style="display:flex;"><span>git-cliff --context | jq &#39;.[0].version&#39;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># Get the version for the next release (based on what has been committed since)
</span></span><span style="display:flex;"><span>git-cliff --bumped-version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># Generate the changelog for that next release
</span></span><span style="display:flex;"><span>git-cliff --bump -o CHANGELOG.md
</span></span></code></pre></div><p>Whichever Git Forge you use, these commands should allow you to create a changelog and release process that works for your platform! Happy crafting!</p>
]]></content></item><item><title>Tip: Zigbee Device Stuck During Pairing</title><link>https://fhoekstra.eu/posts/tip-zigbee-device-stuck-during-pairing/</link><pubDate>Mon, 29 Sep 2025 19:09:28 +0200</pubDate><guid>https://fhoekstra.eu/posts/tip-zigbee-device-stuck-during-pairing/</guid><description>&lt;h2 id="quick-tip"&gt;Quick tip&lt;/h2&gt;
&lt;p&gt;I recently replaced the batteries on two of my Zigbee Tradfri remotes and had
to pair them again to ZHA in Home Assistant because they had been offline for a while.&lt;/p&gt;
&lt;img src="./interview-complete-configuring.jpeg" alt="Screenshot from Home Assistant showing `Interview complete - Configuring` state of a Zigbee device" class="center" style="border-radius: 8px;" /&gt;
&lt;p&gt;I noticed the same thing happening for both devices: they got stuck on the &lt;code&gt;Interview complete - Configuring&lt;/code&gt; step. They are different models, so this made me suspect a software problem. I had a look and found &lt;a href="https://community.home-assistant.io/t/solution-workaround-for-zigbee-zha-devices-stuck-in-interview-complete-configuring/777247"&gt;this topic on the Home Assistant community forum&lt;/a&gt;.&lt;/p&gt;</description><content type="html"><![CDATA[<h2 id="quick-tip">Quick tip</h2>
<p>I recently replaced the batteries on two of my Zigbee Tradfri remotes and had
to pair them again to ZHA in Home Assistant because they had been offline for a while.</p>

    <img src="./interview-complete-configuring.jpeg"  alt="Screenshot from Home Assistant showing `Interview complete - Configuring` state of a Zigbee device"  class="center"  style="border-radius: 8px;"  />


<p>I noticed the same thing happening for both devices: they got stuck on the <code>Interview complete - Configuring</code> step. They are different models, so this made me suspect a software problem. I had a look and found <a href="https://community.home-assistant.io/t/solution-workaround-for-zigbee-zha-devices-stuck-in-interview-complete-configuring/777247">this topic on the Home Assistant community forum</a>.</p>
<p>In it, 2 potential root causes with their respective workarounds are proposed:</p>
<ol>
<li>Cause: The Zigbee device is falling asleep.</li>
</ol>
<ul>
<li>Workaround: Keep pressing one of its regular buttons to keep it awake during pairing</li>
</ul>
<ol start="2">
<li>Cause: The ZHA controller is missing a step or edgecase or the device is not sending the appropriate signal when it is paired.</li>
</ol>
<ul>
<li>Workaround: When you get to the Interview complete step, go to <code>Integrations -&gt; ZHA -&gt; &quot;your coordinator&quot;</code>, press the 3 dots next to it and select: <code>Reload</code>. Afterwards, you&rsquo;ll see your device added to ZHA.</li>
</ul>
<p>Number 1 didn&rsquo;t work for my Tradfri remotes, but number 2 did. I hope writing this helps someone else who runs into this.</p>
]]></content></item><item><title>How-to: Cloudnative FerretDB with Automated Recovery from Continuous Backups</title><link>https://fhoekstra.eu/posts/howto-cloudnative-ferretdb-with-automated-recovery-from-continuous-backups/</link><pubDate>Thu, 25 Sep 2025 10:01:41 +0200</pubDate><guid>https://fhoekstra.eu/posts/howto-cloudnative-ferretdb-with-automated-recovery-from-continuous-backups/</guid><description>&lt;h2 id="why"&gt;Why?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.ferretdb.com/"&gt;FerretDB&lt;/a&gt; is an open source compatibility layer that serves a MongoDB-compatible database server, and stores the data in PostgreSQL. I think this is amazing, but I will save the &amp;ldquo;Why FerretDB?&amp;rdquo; talk for another blog.&lt;/p&gt;
&lt;p&gt;This is a technical guide that will walk you through how to get the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FerretDB deployment backed by highly available and secured cloudnative-pg postgres cluster.&lt;/li&gt;
&lt;li&gt;Barman-cloud plugin backups for point-in-time restores&lt;/li&gt;
&lt;li&gt;Automatic recovery on redeploy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In those 3 steps. What this means for me is that I can get a completely open source MongoDB-compatible database that behaves like the rest of &lt;a href="https://github.com/fhoekstra/home-ops"&gt;my Kubernetes homelab&lt;/a&gt;: if I delete the whole cluster and redeploy it from manifests using GitOps, all my data automatically comes back from backups, without needing any manual intervention. This allows me to play with my homelab without worrying about data restore: everything will come back up on a fresh bootstrap, no manual intervention required.&lt;/p&gt;</description><content type="html"><![CDATA[<h2 id="why">Why?</h2>
<p><a href="https://www.ferretdb.com/">FerretDB</a> is an open source compatibility layer that serves a MongoDB-compatible database server, and stores the data in PostgreSQL. I think this is amazing, but I will save the &ldquo;Why FerretDB?&rdquo; talk for another blog.</p>
<p>This is a technical guide that will walk you through how to get the following:</p>
<ol>
<li>FerretDB deployment backed by highly available and secured cloudnative-pg postgres cluster.</li>
<li>Barman-cloud plugin backups for point-in-time restores</li>
<li>Automatic recovery on redeploy</li>
</ol>
<p>In those 3 steps. What this means for me is that I can get a completely open source MongoDB-compatible database that behaves like the rest of <a href="https://github.com/fhoekstra/home-ops">my Kubernetes homelab</a>: if I delete the whole cluster and redeploy it from manifests using GitOps, all my data automatically comes back from backups, without needing any manual intervention. This allows me to play with my homelab without worrying about data restore: everything will come back up on a fresh bootstrap, no manual intervention required.</p>
<h2 id="step-0-prerequisites">Step 0: Prerequisites</h2>
<ul>
<li>An S3-compatible object storage. Either get one <a href="https://european-alternatives.eu/alternative-to/amazon-s3">from a cloud provider near you</a>, or set up something like <a href="https://garagehq.deuxfleurs.fr/">Garage</a> or <a href="https://github.com/versity/versitygw">Versity Gateway</a> on your NAS. If you get a cloud-provided object storage, make sure to get one that is not too expensive per write transaction, as postgres will be writing there constantly.</li>
<li>A Kubernetes cluster (I&rsquo;m using Kubernetes 1.34 on <a href="https://www.talos.dev/">Talos</a> 1.11 at the time of writing)</li>
<li>cloudnative-pg operator installed on it: <a href="https://cloudnative-pg.io/documentation/current/">one of these commands should get you going</a> (I&rsquo;m running 1.27 at the moment)</li>
<li>barman-cloud plugin for backups of cloudnative-pg provisioned databases. Check the <a href="https://cloudnative-pg.io/plugin-barman-cloud/docs/installation/">instructions here</a>. I tested this with 0.6.0.</li>
<li>Both cloudnative-pg and barman-cloud require cert-manager to be installed on the cluster</li>
<li>In addition, to run postgresql databases, you need some kind of provisioner to provision volumes, preferably on local host storage. Check out <a href="https://github.com/democratic-csi/democratic-csi">democratic-csi</a> or <a href="https://openebs.io/docs/concepts/data-engines/localstorage">openebs-local</a></li>
</ul>
<p>With that out of the way, let&rsquo;s get started.</p>
<h2 id="step-1-ferretdb-backed-by-cloudnative-pg">Step 1: FerretDB backed by cloudnative-pg</h2>
<p>I will be sharing YAML throughout this tutorial. You can copy it to your machine, and then run <code>kubectl apply -f filename.yaml</code> to apply it to your kubernetes cluster, or put it in your GitOps repo, whatever you want.</p>
<p>The following YAML gives you a highly available cloudnative-pg cluster of 3 nodes, with 2 FerretDB servers in front to serve your application with MongoDB-compatible requests.
Feel free to change these numbers, or add a resources block, if you prefer.</p>
<p>The comments explain some of the how and why.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#75715e"># yaml-language-server: $schema=https://github.com/datreeio/CRDs-catalog/raw/refs/heads/main/postgresql.cnpg.io/cluster_v1.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-pg-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Keep this in sync with the correct image for FerretDB itself</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">#  read FerretDB release notes and upgrade them together</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">imageName</span>: <span style="color:#ae81ff">ghcr.io/ferretdb/postgres-documentdb:17-0.106.0-ferretdb-2.5.0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">instances</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># postgres-documentdb needs these IDs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresUID</span>: <span style="color:#ae81ff">999</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresGID</span>: <span style="color:#ae81ff">999</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">storage</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">size</span>: <span style="color:#ae81ff">10Gi</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># This should be the name of the storage class </span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># as configured in your provisioner</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">local-hostpath</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresql</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">shared_preload_libraries</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_cron</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_documentdb_core</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_documentdb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># pg_cron needs to know which database FerretDB uses</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cron.database_name</span>: <span style="color:#ae81ff">ferretDB</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># These parameters are necessary to run ferretdb without superuser access</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Copied from https://github.com/FerretDB/documentdb/blob/ferretdb/packaging/10-preload.sh</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableCompact</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableLetAndCollationForQueryMatch</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableNowSystemVariable</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableSortbyIdPushDownToPrimaryKey</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableSchemaValidation</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableBypassDocumentValidation</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableUserCrud</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.maxUserLimit</span>: <span style="color:#e6db74">&#34;100&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pg_hba</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># pg_cron always runs as `postgres`</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">host ferretDB postgres localhost trust</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># This is needed to prevent fe_sendauth error</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">host ferretDB ferret localhost trust</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">bootstrap</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">initdb</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">database</span>: <span style="color:#ae81ff">ferretDB</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">ferret</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">postInitApplicationSQL</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">create extension if not exists pg_cron;</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">create extension if not exists documentdb cascade;</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">grant documentdb_admin_role to ferret;</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># Keep this in sync with the correct image for the postgresql cluster,</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">#  always read FerretDB release notes and upgrade them together</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/ferretdb/ferretdb:2.5.0</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">27017</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">FERRETDB_POSTGRESQL_URL</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#75715e"># This secret gets automatically generated by cloudnative-pg</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-pg-cluster-app </span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">uri</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-service</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">port</span>: <span style="color:#ae81ff">27017</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">targetPort</span>: <span style="color:#ae81ff">27017</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">type</span>: <span style="color:#ae81ff">NodePort</span>
</span></span></code></pre></div><p>This is a bit longer than <a href="https://blog.ferretdb.io/run-ferretdb-postgres-documentdb-extension-cnpg-kubernetes/">the basic example</a> that was on FerretDB&rsquo;s blog earlier this year, but that is because we need to run postgres without connecting as the <code>postgres</code> superuser.</p>
<p>After applying, watch the pods come up one by one:</p>
<p><code>kubectl -n ferretdb get pods -w</code></p>
<p>After startup is done, check it all works by running:</p>
<p><code>kubectl get cluster.postgresql.cnpg.io -n ferretdb</code></p>
<p>And check that it says: <code>Cluster in healthy state</code> under <code>STATUS</code> like in the below example output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>NAMESPACE   NAME                  AGE     INSTANCES   READY   STATUS                     PRIMARY
</span></span><span style="display:flex;"><span>ferretdb    ferretdb-pg-cluster   33h     3           3       Cluster in healthy state   ferretdb-pg-cluster-1
</span></span></code></pre></div><p>This is <a href="https://www.gabrielebartolini.it/articles/2024/03/cloudnativepg-recipe-3-what-no-superuser-access/">great for security</a>, and is necessary to work with the way cloudnative-pg handles backups in our case.</p>
<p>Try out your FerretDB service now: just connect to the node it is running on (since we used NodePort), or if you&rsquo;re using a cloud Kubernetes outside your home network, change that NodePort to ClusterIP and use <a href="https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/#forward-a-local-port-to-a-port-on-the-pod">kubectl port-forward</a>. You could use any Mongo client you wish, this is what it looks like with mongosh:
<code>mongosh 'mongodb://ferret:the-password-from-the-cloudnative-pg-secret@ip-of-the-node-it-is-running-on-or-localhost-if-port-forwarding:27017'</code></p>
<p>Then <a href="https://www.mongodb.com/docs/mongodb-shell/run-commands/">run some Mongo commands</a>.</p>
<h2 id="step-2-enable-backups">Step 2: Enable backups</h2>
<p>We will use the barman-cloud plugin to take backups. You may read about a legacy way of taking backups with cloudnative-pg, but there are 2 reasons why we use the plugin instead: the legacy way will be removed from the next cloudnative-pg release; and the legacy way requires barman-cloud (the backup tool) to be a part of the postgres image used. Our FerretDB provided image does not have barman-cloud.
Luckily the new way with the plugin creates a separate container with barman-cloud and adds it to each database pod, so that it works regardless of which postgres container is running.</p>
<p>Postgres backups come in multiple flavors. Today we&rsquo;ll look at the 2 that barman provides: base backups are like &ldquo;save points&rdquo; that can be restored from scratch in full. Write-Ahead-Logs (WALs) backup is a log of all the database transactions that can be replayed. These can be used to replay all the way to the latest state, or up to some point in time between the earliest base backup and the latest WAL.</p>
<p>So the combination of WALs and base backups give us both point in time restore, and restore up to the latest state, with no or very little data loss in case of an accidental (or intentional) cluster-wide outage. How these 2 should be used, is entirely handled by the tooling. All we need to do, is ensure backups are automatically made, and to specify a point in time to restore to.</p>
<p>So let&rsquo;s get those backups rolling. First we connect the postgres cluster to the S3 bucket using a BarmanObjectStore object:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">s3-creds</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">stringData</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ACCESS_KEY_ID</span>: <span style="color:#ae81ff">your S3 access key ID goes here</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">SECRET_KEY</span>: <span style="color:#ae81ff">your S3 secret key goes here</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#75715e"># yaml-language-server: $schema=https://github.com/datreeio/CRDs-catalog/raw/refs/heads/main/barmancloud.cnpg.io/objectstore_v1.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">barmancloud.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ObjectStore</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-backupstore</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">retentionPolicy</span>: <span style="color:#ae81ff">14d</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">configuration</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">destinationPath</span>: <span style="color:#ae81ff">s3://bucketname/optionalsubfolder/</span> <span style="color:#75715e"># Change this</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Change the endpoint URL to whatever your cloud provider told you to use</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># NOTE: should be https if using cloud bucket</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">endpointURL</span>: <span style="color:#ae81ff">http://versity.storage.svc.cluster.local:7070 </span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">s3Credentials</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">accessKeyId</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">s3-creds</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">key</span>: <span style="color:#ae81ff">ACCESS_KEY_ID</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">secretAccessKey</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">s3-creds</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">key</span>: <span style="color:#ae81ff">SECRET_KEY</span>
</span></span></code></pre></div><p>Fill in the following S3 connection data in the above YAML before applying:</p>
<ul>
<li>credentials:
<ul>
<li>access key ID</li>
<li>secret key</li>
</ul>
</li>
<li>bucket name (and optional subfolder) NOTE: ensure bucket exists first</li>
<li>endpoint URL</li>
</ul>
<p>And never commit unencrypted secrets to your Git repo (the <code>ObjectStore</code> is safe but the <code>Secret</code> should be encrypted with <a href="https://github.com/getsops/sops">SOPS</a> or filled using <a href="https://external-secrets.io/latest/">external-secrets</a> if you are doing GitOps). Alternatively, <code>.gitignore</code> it.</p>
<p>Then add this <code>plugins</code> section to the <code>spec</code> of your postgresql cluster:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#75715e"># yaml-language-server: $schema=https://github.com/datreeio/CRDs-catalog/raw/refs/heads/main/postgresql.cnpg.io/cluster_v1.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-pg-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  [<span style="color:#ae81ff">...]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">plugins</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">barman-cloud.cloudnative-pg.io</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">isWALArchiver</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">barmanObjectName</span>: <span style="color:#ae81ff">ferretdb-backupstore</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">serverName</span>: <span style="color:#ae81ff">pg</span>
</span></span></code></pre></div><p>Check that the barman-cloud pods are all started after a few seconds. You should see <code>2/2</code> for every pod that is part of the cluster under the <code>READY</code> column of the output of the following command:</p>
<p><code>kubectl -n ferretdb get pods</code></p>
<p>Like the <code>ferretdb-pg-cluster</code> pods here:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>NAME                              READY   STATUS    RESTARTS        AGE
</span></span><span style="display:flex;"><span>ferretdb-79dccb7d5d-8kbnq         1/1     Running   0               34h
</span></span><span style="display:flex;"><span>ferretdb-79dccb7d5d-wnlc9         1/1     Running   0               34h
</span></span><span style="display:flex;"><span>ferretdb-pg-cluster-1             2/2     Running   0               33h
</span></span><span style="display:flex;"><span>ferretdb-pg-cluster-2             2/2     Running   0               33h
</span></span><span style="display:flex;"><span>ferretdb-pg-cluster-3             2/2     Running   0               33h
</span></span></code></pre></div><p>You can also check your S3 storage to see if the WALs are actually ending up in there.
For example, you could install <a href="https://rclone.org/">rclone</a> and connect it to your cloud storage by running <code>rclone config</code>.
Then afterwards, you could run <code>rclone ls name-of-your-remote:</code> to see all the files in there. You should see files in a <code>wals/</code> subdirectory of the path you configured in your ObjectStore.</p>
<p>So now we have the barman-cloud containers streaming the WALs to your cloud object storage. But in order to be able to restore, we also need a &ldquo;save point&rdquo; or base backup. I like to set this up in such a way that it is taken periodically, for example every night, and cloudnative-pg makes this very easy. No cronjob needed, just this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#75715e"># yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/postgresql.cnpg.io/scheduledbackup_v1.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ScheduledBackup</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backupOwnerReference</span>: <span style="color:#ae81ff">self</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cluster</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-pg-cluster</span> <span style="color:#75715e"># Should be the name of your postgres cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>: <span style="color:#e6db74">&#34;0 0 1 * * *&#34;</span> <span style="color:#75715e"># This is a slightly unusual cron format</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">immediate: true # Means</span>: <span style="color:#ae81ff">take a snapshot right now as well</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">method</span>: <span style="color:#ae81ff">plugin</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pluginConfiguration</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">barman-cloud.cloudnative-pg.io</span>
</span></span></code></pre></div><p>Check that it is applied:</p>
<p><code>kubectl -n ferretdb get scheduledbackup</code></p>
<p>Check that it spawns a regular <code>Backup</code> object:</p>
<p><code>kubectl -n ferretdb get scheduledbackup</code></p>
<p>And check the phase. You can watch it to not have to keep refreshing by adding <code>-w</code>. Eventually the <code>PHASE</code> should be <code>completed</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>NAMESPACE   NAME                        AGE     CLUSTER               METHOD              PHASE       ERROR
</span></span><span style="display:flex;"><span>ferretdb    ferretdb-20250924091153     33h     ferretdb-pg-cluster   plugin              completed   
</span></span></code></pre></div><p>And you should see a recovery window in the status of your <code>ObjectStore</code>:
<code>kubectl -n ferretdb describe objectstore ferretdb-backupstore</code>
On the bottom of the output you should see something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Status:
</span></span><span style="display:flex;"><span>  Server Recovery Window:
</span></span><span style="display:flex;"><span>    Pg:
</span></span><span style="display:flex;"><span>      First Recoverability Point:   2025-09-24T09:12:01Z
</span></span><span style="display:flex;"><span>      Last Successful Backup Time:  2025-09-25T01:00:07Z
</span></span></code></pre></div><p>And that&rsquo;s it. Now you could leave it running for a while, write some data, delete some data and try out point in time restore using <a href="https://cloudnative-pg.io/documentation/1.27/recovery/#pitr-from-an-object-store">cloudnative-pg documentation</a></p>
<p>But for this tutorial, we will go to the initially promised final step.</p>
<h1 id="step-3-automatic-recovery">Step 3. Automatic recovery</h1>
<p>It is important that your backups are properly wired up and you have a recovery window in your <code>ObjectStore</code> before you start this step.</p>
<p>The usecase is the following: imagine your whole kubernetes cluster is wiped. Maybe there&rsquo;s water damage to your house, a lightning hit, or (more common in the homelab) you have so seriously messed up your cluster that it is easier to start over and re-apply all the YAML files you have been keeping track of, than to actually fix the problem. Or if you are using a proper GitOps solution like <a href="https://fluxcd.io/">FluxCD</a>, applying all the YAMLs is the same as just deploying Flux into a fresh cluster.</p>
<p>Let&rsquo;s set up our postgres cluster configuration to automatically restore from the <code>ObjectStore</code>. Add the lines marked with <code>+</code> to the postgres cluster document (without the +), comment out the <code>initdb</code> section and re-apply it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#75715e"># yaml-language-server: $schema=https://github.com/datreeio/CRDs-catalog/raw/refs/heads/main/postgresql.cnpg.io/cluster_v1.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ferretdb-pg-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">ferretdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+ annotations</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+   # required for seamless bootstrap</span>: <span style="color:#ae81ff">https://github.com/cloudnative-pg/cloudnative-pg/issues/5778#issuecomment-2783417464</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+   cnpg.io/skipEmptyWalArchiveCheck</span>: <span style="color:#e6db74">&#34;enabled&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Keep this in sync with the correct image for FerretDB itself</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">#  read FerretDB release notes and upgrade them together</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">imageName</span>: <span style="color:#ae81ff">ghcr.io/ferretdb/postgres-documentdb:17-0.106.0-ferretdb-2.5.0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">instances</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># postgres-documentdb needs these IDs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresUID</span>: <span style="color:#ae81ff">999</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresGID</span>: <span style="color:#ae81ff">999</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">storage</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">size</span>: <span style="color:#ae81ff">10Gi</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># This should be the name of the storage class as configured in your provisioner</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">local-hostpath</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresql</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">shared_preload_libraries</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_cron</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_documentdb_core</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pg_documentdb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># pg_cron needs to know which database FerretDB uses</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cron.database_name</span>: <span style="color:#ae81ff">ferretDB</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># The parameters below are necessary to run ferretdb without superuser access</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Copied from https://github.com/FerretDB/documentdb/blob/ferretdb/packaging/10-preload.sh</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableCompact</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableLetAndCollationForQueryMatch</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableNowSystemVariable</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableSortbyIdPushDownToPrimaryKey</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableSchemaValidation</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableBypassDocumentValidation</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.enableUserCrud</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">documentdb.maxUserLimit</span>: <span style="color:#e6db74">&#34;100&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pg_hba</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># pg_cron always runs as `postgres`</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">host ferretDB postgres localhost trust</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># This is needed to prevent fe_sendauth error</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">host ferretDB ferret localhost trust</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">bootstrap</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+   recovery</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+     source</span>: <span style="color:#75715e">&amp;source</span> <span style="color:#ae81ff">pg</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    initdb:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#      database: ferretDB</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#      owner: ferret</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#      postInitApplicationSQL:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#        - create extension if not exists pg_cron;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#        - create extension if not exists documentdb cascade;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#        - grant documentdb_admin_role to ferret;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">plugins</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">barman-cloud.cloudnative-pg.io</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">isWALArchiver</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">barmanObjectName</span>: <span style="color:#ae81ff">ferretdb-backupstore</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">serverName</span>: <span style="color:#ae81ff">pg</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+ externalClusters</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+   - name</span>: <span style="color:#ae81ff">pg</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+     plugin</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+       name</span>: <span style="color:#ae81ff">barman-cloud.cloudnative-pg.io</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+       parameters</span>:
</span></span><span style="display:flex;"><span><span style="color:#f92672">+         barmanObjectName</span>: <span style="color:#ae81ff">ferretdb-backupstore</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">+         serverName</span>: <span style="color:#ae81ff">pg</span>
</span></span></code></pre></div><p>Don&rsquo;t forget the annotation! Without it, barman-cloud will refuse to continue writing backups to the same ObjectStore and serverName after recovery. It is generally safe to use this annotation<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Now check this has applied successfully:</p>
<p><code>kubectl get cluster.postgresql.cnpg.io -n ferretdb</code></p>
<p><code>kubectl describe cluster.postgresql.cnpg.io -n ferretdb</code></p>
<p>(you can also shorten these to just use <code>cluster</code> if you have no other resources named <code>cluster</code> except the cnpg cluster.)</p>
<p>If it has, go ahead and delete your entire postgres cluster:</p>
<p><code>kubectl delete -n ferretdb cluster.postgresql.cnpg.io ferretdb-pg-cluster --force</code></p>
<p>Check that all the postgres cluster pods and PVCs are gone (you might have to <code>kubectl delete --force</code> some of them):</p>
<p><code>kubectl -n ferretdb get pods</code></p>
<p><code>kubectl -n ferretdb get pvc</code></p>
<p>If they are all gone, here comes the test: re-apply the postgresql YAML and it should start a full-recovery:</p>
<p><code>kubectl apply -f postgres-cluster.yaml</code></p>
<p>Check the pods:</p>
<p><code>kubectl -n ferretdb get pods -w</code>
Check the logs:</p>
<p><code>kubectl -n ferretdb logs name-of-the-pod</code></p>
<p>And soon, your cluster should be back and healthy. Connect to it as in Step 1 and check that your data is restored. Make a happy dance, then go to bed and sleep soundly, knowing your data is safe, no manual intervention is required for restore, and you are using a completely open source document database solution.</p>
<p>If things change and you want to check how I am running FerretDB, go to <a href="https://github.com/fhoekstra/home-ops">github.com/fhoekstra/home-ops</a>, press <code>t</code> and type <code>ferretdb</code>.</p>
<p>If you found an error, I would love to accept your PR to this blog at <a href="https://codeberg.org/fhoekstra/blog">https://codeberg.org/fhoekstra/blog</a></p>
<p>If you need help setting this up at home, come to the <a href="https://github.com/home-operations">Home Operations</a> <a href="https://discord.gg/home-operations">Discord</a>.</p>
<h2 id="attribution">Attribution</h2>
<p>I did not come up with any of this myself, I just combined the ideas from some amazing people around me, tested it and wrote it down. Special thanks go to <a href="https://github.com/eaglesemanation">@eaglesemanation</a> for figuring out how to run FerretDB against cloudnative-pg without superuser access. That made this setup possible.</p>
<p>I should also thank <a href="https://github.com/tholinka">@tholinka</a> and <a href="https://github.com/phycoforce">@Phycoforce</a> from the <a href="https://discord.gg/home-operations">Home-Operations</a> community for their help with barman-cloud backups. Their home-ops repos are a great resource.</p>
<p>You can browse many more repos using cloudnative-pg and barman-cloud by searching <a href="https://kubesearch.dev">kubesearch.dev</a> to get ideas of other ways to set this up.</p>
<hr>
<h2 id="footnotes">Footnotes</h2>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>But if you are going to do a major upgrade (for example to Postgresql 18), you might want to do that using a fresh cluster, with its own new <code>serverName</code> instead of keeping the backups in the same place. Thanks to <a href="https://github.com/aclerici38">@MASTERBLASTER</a> in the <a href="https://discord.gg/home-operations">Home-Operations</a> community for this addition.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content></item></channel></rss>