<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Caius&#x27; Lab</title>
      <link>https://caius.dev/</link>
      <description></description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://caius.dev/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Thu, 11 Jun 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Making a Digital Clock; An Attempt at Integrating Hardware, Software and 3D Printing</title>
          <pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/digital-clock/</link>
          <guid>https://caius.dev/blog/digital-clock/</guid>
          <description xml:base="https://caius.dev/blog/digital-clock/">&lt;p&gt;For the last few years, we&#x27;ve been using a Comcast TV Box as a clock for the living room.
It hasn&#x27;t been used for its original purpose in years, but the clock was very useful, so it lived on for this reason alone.
To save some space (and power), I decommissioned that box about a month ago to make room for a networking mini-rack.&lt;&#x2F;p&gt;
&lt;p&gt;However, my partner really missed the illuminted clock in the living room.
Whatever clocks we had lying around were not illuminated as they were battery powered, so none were a good replacement because of this.
To fix this, I decided to try and build one using off the shelf components, and a 3D printed case.
The final result can be seen below.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.2bd5b9bd3436fef1.jpg&quot; alt=&quot;Final clock as built&quot;
     width=&quot;1280&quot; height=&quot;829&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.9133aa6e386b7906.jpg 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.82d13f1e04d706c6.jpg 784w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.2482963dfc43bc4d.jpg 1280w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.5cdd81d4f6378c0a.jpg 1920w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;clock.65152d760ffe02b5.jpg 2560w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;h1 id=&quot;inspiration-and-design&quot;&gt;Inspiration and design&lt;a class=&quot;post-anchor&quot; href=&quot;#inspiration-and-design&quot; aria-label=&quot;Anchor link for: inspiration-and-design&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The clock was inspired by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;1344276-esptimecast-wifi-clock-weather-display&quot;&gt;this 3D Model&lt;&#x2F;a&gt;.
However, I wanted a different look, and the project seemed easy enough to try out by myself.&lt;&#x2F;p&gt;
&lt;p&gt;The design goals were, as follows:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Illuminated&lt;&#x2F;li&gt;
&lt;li&gt;Powered externally. I don&#x27;t need another thing in my life that needs charging.&lt;&#x2F;li&gt;
&lt;li&gt;Self adjusting, so I don&#x27;t have to worry about setting the correct time and DST changes.&lt;&#x2F;li&gt;
&lt;li&gt;Shows the local temperature; because why not.&lt;&#x2F;li&gt;
&lt;li&gt;Decent aesthetic (at least as far as my design skills go).&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;parts&quot;&gt;Parts&lt;a class=&quot;post-anchor&quot; href=&quot;#parts&quot; aria-label=&quot;Anchor link for: parts&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;I had a set of off-brand WeMos D1 Mini-style ESP dev boards I got off Amazon.
They have WiFi, so they seemed as the perfect candidate for the brains of the project.&lt;&#x2F;p&gt;
&lt;p&gt;The display is a 4 module MAX7219, with red LEDs, giving me a resolution of 32x8 pixels.
I bought &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B09CGWZRYG&quot;&gt;a cheap set of 3 from Amazon&lt;&#x2F;a&gt;, so I had a few tries in case the assembly went wrong.
To wire it all permanently, the idea was to use a protoboard to solder everything into one assembly.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;typeface&quot;&gt;Typeface&lt;a class=&quot;post-anchor&quot; href=&quot;#typeface&quot; aria-label=&quot;Anchor link for: typeface&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The goal was to have a typeface that could show all 10 digits, in a 3x6 size.
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;moonbench.xyz&#x2F;projects&#x2F;tiny-pixel-art-fonts&#x2F;&quot;&gt;This blog post&lt;&#x2F;a&gt; provides fantastic examples, and the 3x6 examples posted were a great starting point.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the design for the 10 digits that I landed on:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;typeface.dab2fb5fc9fdd53e.png&quot; alt=&quot;3x6 bitmap typeface&quot;
     width=&quot;468&quot; height=&quot;96&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;The glyphs are legible, and they cannot be confused with one another.
Two of the digits proved to be problematic, in particular 4 and 9.
The original design was the following:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;4-and-9.769239de4cf93c6f.png&quot; alt=&quot;Original designs for 4 and 9 glyphs.&quot;
     width=&quot;84&quot; height=&quot;96&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;However, the 4 looks like a 9 that has lost a pixel, either because it&#x27;s out, or because I messed up the encoding.
The final design of a more traditional 4, and a 9 that&#x27;s the same glyph as 6, rotated 180 degrees looks a lot better, and leaves no room for confusion.
At this resolution, it works well enough.&lt;&#x2F;p&gt;
&lt;p&gt;The 5 used to be a 180 degree rotated variant of the 2 glyph.
However, I didn&#x27;t like the shape of it, so I added one more pixel, and it makes all the difference.
You can see here the old and new 5 glyphs, side by side:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;5-old-new.ca090c9559597ab5.png&quot; alt=&quot;Original and new design for the 5 glyph.&quot;
     width=&quot;84&quot; height=&quot;96&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;&quot;
     loading=&quot;lazy&quot;&gt;
&lt;h1 id=&quot;ui-considerations&quot;&gt;UI considerations&lt;a class=&quot;post-anchor&quot; href=&quot;#ui-considerations&quot; aria-label=&quot;Anchor link for: ui-considerations&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;layout&quot;&gt;Layout&lt;a class=&quot;post-anchor&quot; href=&quot;#layout&quot; aria-label=&quot;Anchor link for: layout&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now that I had the the glyphs design, the next step was to come up with a layout.
I went with a 3 pixel wide typeface, because this gives me room for 7 digits on the 32 wide display, including the colon for the time, and 1 pixel to represent degree symbol.
All characters are spaced 1 pixel apart.&lt;&#x2F;p&gt;
&lt;p&gt;The final layout looks like this:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;face-layout.e9ba5ee4a9eed8ea.png&quot; alt=&quot;Layout of the clock face.&quot;
     width=&quot;384&quot; height=&quot;96&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;The temperature is left aligned, so under normal weather conditions in the Pacific Northwest, only the left 2 digits are used for the majority of the time.
The left digit will be used for a minus sign if the temperatures go negative.
Luckily, I don&#x27;t have to worry about that over here, if I stick to Fahrenheit&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;animation&quot;&gt;Animation&lt;a class=&quot;post-anchor&quot; href=&quot;#animation&quot; aria-label=&quot;Anchor link for: animation&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Since I have a grid display, and a microcontroller, having the digits just change would be quite boring.
I think having a nice animation when changing digits would be a nice touch.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;blog&#x2F;digital-clock&#x2F;assets&#x2F;animation.gif&quot; alt=&quot;Digit change animation.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;feedback-and-error-conditions&quot;&gt;Feedback and error conditions&lt;a class=&quot;post-anchor&quot; href=&quot;#feedback-and-error-conditions&quot; aria-label=&quot;Anchor link for: feedback-and-error-conditions&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;To avoid having to manually set the time, and to compensate for the known not-so-great quality of the RTC on the board, the clock will synchronize with NTP once hourly&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-2-1&quot;&gt;&lt;a href=&quot;#fn-2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
When this is happening, the display will not update, as the update is synchronous.&lt;&#x2F;p&gt;
&lt;p&gt;This presents 2 challenges.
First, any animation would &quot;freeze&quot; or not work while the sync is in progress.
This is an easy fix, as I don&#x27;t allow update to happen within 10 seconds from the top of the minute.
This means that time updates can only happen between seconds 10 and 50 of every minute, avoiding a display freeze in the middle of an animation should the 2 coincide at the same time.&lt;&#x2F;p&gt;
&lt;p&gt;The second issue of the display freezing, is that I will not know if it&#x27;s frozen because of a problem, or because it&#x27;s synchronizing.
This is fixed, by adding a synchronization symbol to the clock face.
The symbol is a &quot;double height&quot; colon.
It blends in nicely aesthetically, and doesn&#x27;t interfere with the main function.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;synchronizing-display.e830d45224402cc7.png&quot; alt=&quot;Layout of the clock face when a sync is in progress.&quot;
     width=&quot;204&quot; height=&quot;96&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;The double height colon also serves as an indicator that the time sync failed on the last attempt.
The colon will blink every second (like the normal state).
This still allows the clock to function, while signalling that the displayed time might not be accurate.
This state will persist until a time sync is successful.
When the sync fails, we&#x27;ll keep retrying every 10 minutes, to minimize the time when we have &quot;degraded time.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;The final piece of the puzzle is the temperature.
If the clock fails to fetch it, the temperature display will be removed from the clock face until we have data.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;caiusb&#x2F;clock&quot;&gt;full code for the firmware is up on GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;case-design&quot;&gt;Case design&lt;a class=&quot;post-anchor&quot; href=&quot;#case-design&quot; aria-label=&quot;Anchor link for: case-design&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The case design was probably the more challenging part of the project.
First, this is the first integrated electronics project that I have tackled from scratch.
Secondly, and crucially, I&#x27;m still learning CAD.
Having a &quot;full&quot; assembly was a good way to learn a few tricks.&lt;&#x2F;p&gt;
&lt;p&gt;The electronics will be retained in the clock through friction.
A few test prints showed that the diffuser part fits snugly enough that this would be fine.
Also, since I was using off-the-shelf components in ways they probably never were designed to integrate, coming up with a different mounting solution looked quite daunting.&lt;&#x2F;p&gt;
&lt;p&gt;To aid with 3D printing, the case should also require minimal, to no supports.
The end result requires no supports, so I&#x27;m happy with the result.
The final designed is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;1749367-mimimalist-digital-clock&quot;&gt;shared on Printables&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;display-diffuser&quot;&gt;Display Diffuser&lt;a class=&quot;post-anchor&quot; href=&quot;#display-diffuser&quot; aria-label=&quot;Anchor link for: display-diffuser&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For the diffuser, one solution was to 3D print a thin 2-3 layer cover that goes over the display.
The gaps in the layers would still allow the digits to be seen, while aiding readability by improving contrast.
I did a few test prints, however the end result was too dim for my liking, and I had to power the LEDs at full brightness for the contrast to be decent.
So I needed something more transparent.&lt;&#x2F;p&gt;
&lt;p&gt;Luckily I had some transparent PETG that I bought years ago, and never opened.
Printing a 3 layer diffuser over the display removed the background &quot;noise&quot; of the turned off pixels and allowed most of the light to shine through.
The display is still legible in bright daylight, even at the lowest setting, without turning the room red at night.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;integration&quot;&gt;Integration&lt;a class=&quot;post-anchor&quot; href=&quot;#integration&quot; aria-label=&quot;Anchor link for: integration&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The final integration was mostly around getting the electronics all sorted out.
The most challenging part was removing the existing 90 degree header from the display module, so I could use a straight header to connect to the protoboard that was carrying the MCU.&lt;&#x2F;p&gt;
&lt;p&gt;Once that hurdle was over, the wiring was done on a piece of proto board, and then everything was soldered together as one unit.
I didn&#x27;t have any Kapton tape, so electrical tape had to do to avoid any shortcircuits between the stacked headers.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, at the last minute, I added a 10K potentiometer to control the brightness, without having to reupload the firmware, together with the code to make this work.
This will allow me to easily tune the brightness when it&#x27;s installed.&lt;&#x2F;p&gt;
&lt;p&gt;Below are 2 images: one of the final electronics assembly, and one with the electronics assembled inside the case.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.3269b31825e44669.jpg&quot; alt=&quot;Electronics module as fully assembled&quot;
     width=&quot;1280&quot; height=&quot;960&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.b6cfd3426cb3d780.jpg 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.ceefe5a6bd09efb9.jpg 784w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.2645c3217800e8b9.jpg 1280w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.162ae93e4af4e1f6.jpg 1920w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics.e046c0fb1077edcf.jpg 2560w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.2f4df965e1c1feaa.jpg&quot; alt=&quot;Electronics module installed in the case&quot;
     width=&quot;1280&quot; height=&quot;960&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.23dca298423d7940.jpg 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.12d6d1bb4929939c.jpg 784w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.e2827c9d292b03bb.jpg 1280w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.6da156947c3a4b89.jpg 1920w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;electronics-in-case.1c22d37af939a36f.jpg 2560w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;h1 id=&quot;final-results&quot;&gt;Final results&lt;a class=&quot;post-anchor&quot; href=&quot;#final-results&quot; aria-label=&quot;Anchor link for: final-results&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;It&#x27;s been living under the TV for well over a week now.
It works pretty well for a clock.
The time is accurate, and I haven&#x27;t seen any synchronizing issues.
Finally, I only saw one issue with the temperature update, and that also behaved as expected by dropping the temperature from the display.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusions&quot; aria-label=&quot;Anchor link for: conclusions&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;It was a fun build.
It pushed my coding skills, as it&#x27;s been a while since I&#x27;ve written C++, or worked extensively with microcontrollers&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-3-1&quot;&gt;&lt;a href=&quot;#fn-3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
Coming up with a parametric design that had 4 total pieces (front bezel, diffuser, main body, and the rear cover) was challenging.
It worked out in the end, and only one hole was misaligned!&lt;&#x2F;p&gt;
&lt;p&gt;The end product meets 100% of my original design criteria, and is a fully functional and setup-free clock.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;errata&quot;&gt;Errata&lt;a class=&quot;post-anchor&quot; href=&quot;#errata&quot; aria-label=&quot;Anchor link for: errata&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026-06-11&lt;&#x2F;strong&gt;: Updated the face layout images to use the correct 8 glyph.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;Negative temperatures below 99 degrees (F or C) are intentionally not supported. I think I&#x27;ll have other things to worry about if that&#x27;s ever needed. &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;The once hourly part has been arbitrarily chosen. I did not measure the precision of the on-board RTC, but it seems reasonable that it keep to within a minute for one hour. &lt;a href=&quot;#fr-2-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-3&quot;&gt;
&lt;p&gt;An LLM was used to speed up the coding part. &lt;a href=&quot;#fr-3-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</description>
      </item>
      <item>
          <title>Backing up Persistent Volume Claims with k8up</title>
          <pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/pvc-backups/</link>
          <guid>https://caius.dev/blog/pvc-backups/</guid>
          <description xml:base="https://caius.dev/blog/pvc-backups/">&lt;p&gt;Previously, I wrote about &lt;a href=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;blog&#x2F;self-hosted-postgres&#x2F;&quot;&gt;backing up Postgres to S3 using CNPG and Barman Cloud&lt;&#x2F;a&gt;.
This is enough if all the data you want to persist is in Postgres.
However, if you have any other volumes, this will not be enough.
In this post, I&#x27;ll go over setting up backups with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;k8up.io&#x2F;&quot;&gt;k8up&lt;&#x2F;a&gt;, and what the restore process looks like.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;why-k8up&quot;&gt;Why k8up?&lt;a class=&quot;post-anchor&quot; href=&quot;#why-k8up&quot; aria-label=&quot;Anchor link for: why-k8up&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;k8up (pronounced &quot;ketchup&quot;) is a Kubernetes Operator distributed via a Helm chart.
By default, it backs up all Persistent Volume Claims (PVCs) marked as &lt;code&gt;ReadWriteMany&lt;&#x2F;code&gt;, &lt;code&gt;ReadWriteOnce&lt;&#x2F;code&gt;, or with a certain label.
It uses &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;restic.net&#x2F;&quot;&gt;restic&lt;&#x2F;a&gt;, so it can write to object storage.
For this blog post, I&#x27;m using Amazon S3 as the destination, but self-hosted options like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;garagehq.deuxfleurs.fr&#x2F;&quot;&gt;Garage&lt;&#x2F;a&gt; should also work.&lt;&#x2F;p&gt;
&lt;p&gt;As to why I chose it, it&#x27;s more mature than &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;backube&#x2F;volsync&quot;&gt;VolSync&lt;&#x2F;a&gt;, and I&#x27;m interested only in backing up PVCs.
The cluster definition is managed with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;helm.sh&#x2F;&quot;&gt;Helm&lt;&#x2F;a&gt;, so &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;velero.io&#x2F;&quot;&gt;Velero&lt;&#x2F;a&gt; seems a bit too heavy for my limited use case.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, there&#x27;s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;longhorn.io&#x2F;&quot;&gt;Longhorn&lt;&#x2F;a&gt;, which can also do backups.
My cluster will be quite resource-constrained, so I&#x27;m not sure the extra overhead of Longhorn will be worth it in the long run.
I&#x27;m OK with an &quot;outage&quot; if I need to restore a PVC from S3, and the extra redundancy doesn&#x27;t seem worth the overhead.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;test-service&quot;&gt;Test service&lt;a class=&quot;post-anchor&quot; href=&quot;#test-service&quot; aria-label=&quot;Anchor link for: test-service&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;To evaluate this, I&#x27;ve created a very simple service that writes the current time to a PVC once a second.
It will give me an idea of how backups perform, and how recovery works.
Here is the Helm template for this service:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; PersistentVolumeClaim&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; time-writer-pvc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  labels&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    must-backup&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;true&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  accessModes&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ReadWriteOnce&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  resources&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    requests&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      storage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; 100Mi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;---&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Pod&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; time-writer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  containers&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; time-writer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      image&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; busybox:1.37&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      imagePullPolicy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; IfNotPresent&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      command&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;&amp;quot;&#x2F;bin&#x2F;sh&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;-c&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      args&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; while true; do date &amp;gt;&amp;gt; &#x2F;data&#x2F;time.log; sleep 1; done&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      volumeMounts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;          mountPath&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  volumes&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      persistentVolumeClaim&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        claimName&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; time-writer-pvc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h1 id=&quot;backup-setup&quot;&gt;Backup setup&lt;a class=&quot;post-anchor&quot; href=&quot;#backup-setup&quot; aria-label=&quot;Anchor link for: backup-setup&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;Once we have this setup, we can start with the backups.
First, we need to create 2 secrets: one for the restic encryption key, and one for the AWS credentials.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-restic-secret-key&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Opaque&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;stringData&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  secretKey&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.secrets.resticSecretKey&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;---&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Opaque&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;stringData&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  access-key-id&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.secrets.awsAccessKey&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  secret-key-id&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.secrets.awsSecretKey&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, we can define the backup we want to take.
For &lt;code&gt;s3&lt;&#x2F;code&gt;, I&#x27;ll use the regional S3 endpoint, &lt;code&gt;s3.us-west-2.amazonaws.com&lt;&#x2F;code&gt;.
For the bucket name, we can also specify a prefix.
In this example, the restic repo lives under the &lt;code&gt;test&lt;&#x2F;code&gt; prefix.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; k8up.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  failedJobsHistoryLimit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  successfulJobsHistoryLimit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  backend&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    repoPasswordSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-restic-secret-key&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secretKey&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    s3&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;s3.us-west-2.amazonaws.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      bucket&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; bucket-name&#x2F;test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      accessKeyIDSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; access-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      secretAccessKeySecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secret-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Once this is done, we can run &lt;code&gt;helm upgrade&lt;&#x2F;code&gt; and we get our backup.
Checking the jobs, they have completed:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ kubectl get jobs -n backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;NAME                                  STATUS     COMPLETIONS   DURATION   AGE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;backup-schedule-test-backup-c9wz6-0   Complete   1&#x2F;1           9s         12m&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And looking at the logs, we get confirmation that it backed up our only file:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T19:45:35Z    INFO    k8up.restic.restic.backup    starting backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T19:45:35Z    INFO    k8up.restic.restic.backup    starting backup for folder    {&amp;quot;foldername&amp;quot;: &amp;quot;time-writer-pvc&amp;quot;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T19:45:35Z    INFO    k8up.restic.restic.backup.command    restic command    {&amp;quot;path&amp;quot;: &amp;quot;&#x2F;usr&#x2F;local&#x2F;bin&#x2F;restic&amp;quot;, &amp;quot;args&amp;quot;: [&amp;quot;backup&amp;quot;, &amp;quot;--host&amp;quot;, &amp;quot;backup-test&amp;quot;, &amp;quot;--json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T19:45:35Z    INFO    k8up.restic.restic.backup.command    Defining RESTIC_PROGRESS_FPS    {&amp;quot;frequency&amp;quot;: 0.016666666666666666}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T19:45:37Z    INFO    k8up.restic.restic.backup.progress    backup finished    {&amp;quot;new files&amp;quot;: 1, &amp;quot;changed files&amp;quot;: 0, &amp;quot;errors&amp;quot;: 0} &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For the final confirmation, we can see the restic repo structure in S3:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ aws s3 ls s3:&#x2F;&#x2F;bucket-names&#x2F;test&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE data&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE index&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE keys&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE snapshots&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03 12:45:35        155 config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h1 id=&quot;recovery&quot;&gt;Recovery&lt;a class=&quot;post-anchor&quot; href=&quot;#recovery&quot; aria-label=&quot;Anchor link for: recovery&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;A backup is only useful if we can recover our data from it.
So let&#x27;s see how this would work.
First, let&#x27;s create a PVC that&#x27;s the destination for our data:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; PersistentVolumeClaim&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; restore-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  annotations&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;    # Setting this to false to exclude from future backups&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    k8up.io&#x2F;backup&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;false&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  accessModes&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ReadWriteOnce&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  resources&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    requests&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      storage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; 100Mi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then, we can define our restore:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; k8up.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Restore&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; restore-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  restoreMethod&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    folder&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      claimName&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; restore-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  backend&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    repoPasswordSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-restic-secret-key&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secretKey&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    s3&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;s3.us-west-2.amazonaws.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      bucket&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; bucket-name&#x2F;test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      accessKeyIDSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; access-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      secretAccessKeySecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secret-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will create a new job, and checking the logs, we can see that it succeeded:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ kubectl logs -n backup-test jobs&#x2F;restore-restore-test -f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;...truncated...&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T20:04:04Z    INFO    k8up.restic.restic.restore.command      restic command  {&amp;quot;path&amp;quot;: &amp;quot;&#x2F;usr&#x2F;local&#x2F;bin&#x2F;restic&amp;quot;, &amp;quot;args&amp;quot;: [&amp;quot;restore&amp;quot;, &amp;quot;be8f37f009f0c910ad5ce8aa067d3dbd4050e7de9c41fde14394555a73adee06:&#x2F;data&#x2F;time-writer-pvc&amp;quot;, &amp;quot;--target&amp;quot;, &amp;quot;&#x2F;restore&amp;quot;]}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T20:04:04Z    INFO    k8up.restic.restic.restore.command      Defining RESTIC_PROGRESS_FPS    {&amp;quot;frequency&amp;quot;: 0.016666666666666666}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T20:04:05Z    INFO    k8up.restic.restic.restore.restic.stdout        restoring snapshot be8f37f0 of [&#x2F;data&#x2F;time-writer-pvc] at 2026-05-03 19:45:35.978401924 +0000 UTC by @backup-test to &#x2F;restore&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T20:04:05Z    INFO    k8up.restic.restic.restore.restic.stdout        Summary: Restored 1 files&#x2F;dirs (3.144 KiB) in 0:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, we can check the PVC directly, and confirm we have data until 19:45:36.
The backup was started at 19:45:35 and finished by 19:45:37.
This gives us a very good RPO of a few seconds, which is great for my use case!&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ kubectl exec -n backup-test -it pvc-inspect -- tail &#x2F;data&#x2F;time.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:27 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:28 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:29 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:30 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:31 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:32 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:33 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:34 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:35 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sun May  3 19:45:36 UTC 2026&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h1 id=&quot;scheduling&quot;&gt;Scheduling&lt;a class=&quot;post-anchor&quot; href=&quot;#scheduling&quot; aria-label=&quot;Anchor link for: scheduling&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The above example only covers a &quot;one-time&quot; backup.
For this to be useful, I need a regular schedule.
This is where the &lt;code&gt;Schedule&lt;&#x2F;code&gt; comes in.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ll start with the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.k8up.io&#x2F;k8up&#x2F;2.15&#x2F;how-tos&#x2F;schedules.html&quot;&gt;example provided in the documentation&lt;&#x2F;a&gt;.
We&#x27;ll back up every 5 minutes, and every hour we&#x27;ll do checking and pruning.
Data retention can be tuned depending on the use case.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; k8up.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Schedule&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; schedule-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  backend&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    repoPasswordSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-restic-secret-key&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secretKey&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    s3&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;s3.us-west-2.amazonaws.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      bucket&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; bucket-name&#x2F;test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      accessKeyIDSecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; access-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      secretAccessKeySecretRef&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;: {{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; .Values.clusterName&lt;&#x2F;span&gt;&lt;span&gt; }}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;-s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secret-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  backup&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    schedule&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;*&#x2F;5 * * * *&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    failedJobsHistoryLimit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    successfulJobsHistoryLimit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  check&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    schedule&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;0 * * * *&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  prune&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    schedule&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;30 * * * *&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    retention&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      keepLast&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      keepDaily&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 7&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will result in one job every 5 minutes:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ kubectl get jobs -n backup-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;NAME                                  STATUS     COMPLETIONS   DURATION   AGE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;backup-schedule-test-backup-7l7sj-0   Complete   1&#x2F;1           10s        3m17s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;backup-schedule-test-backup-l7jkq-0   Complete   1&#x2F;1           10s        8m17s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And for each job, the backup was successful:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:29Z    INFO    k8up.restic.restic.backup       starting backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:29Z    INFO    k8up.restic.restic.backup       starting backup for folder      {&amp;quot;foldername&amp;quot;: &amp;quot;time-writer-pvc&amp;quot;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:29Z    INFO    k8up.restic.restic.backup.command       restic command  {&amp;quot;path&amp;quot;: &amp;quot;&#x2F;usr&#x2F;local&#x2F;bin&#x2F;restic&amp;quot;, &amp;quot;args&amp;quot;: [&amp;quot;backup&amp;quot;, &amp;quot;--json&amp;quot;, &amp;quot;--host&amp;quot;, &amp;quot;backup-test&amp;quot;, &amp;quot;&#x2F;data&#x2F;time-writer-pvc&amp;quot;]}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:29Z    INFO    k8up.restic.restic.backup.command       Defining RESTIC_PROGRESS_FPS    {&amp;quot;frequency&amp;quot;: 0.016666666666666666}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:31Z    INFO    k8up.restic.restic.backup.progress      backup finished {&amp;quot;new files&amp;quot;: 0, &amp;quot;changed files&amp;quot;: 1, &amp;quot;errors&amp;quot;: 0}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2026-05-03T21:40:31Z    INFO    k8up.restic.restic.backup.progress      stats   {&amp;quot;time&amp;quot;: 1.727354075, &amp;quot;bytes added&amp;quot;: 51912, &amp;quot;bytes processed&amp;quot;: 50837}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;Overall, k8up seems to meet my use case.
The setup is straightforward and easy to reason about.
The restore process is also straightforward.&lt;&#x2F;p&gt;
&lt;p&gt;While the example here is very limited, it gives me confidence that the basics work.
The real test will come once it&#x27;s operating on real data, but regular restores and testing should give me the confidence I need there.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Setting up a Homelab: Step 1</title>
          <pubDate>Sun, 19 Apr 2026 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/homelab-step-1/</link>
          <guid>https://caius.dev/blog/homelab-step-1/</guid>
          <description xml:base="https://caius.dev/blog/homelab-step-1/">&lt;p&gt;I am building a Kubernetes cluster out of second-hand Lenovo ThinkCenter machines.
As part of this project, I want to streamline the setup as much as possible, and I&#x27;ll try to automate most steps.
The goal is to have a reproducible setup that will simplify modifications, adding more hardware, etc.&lt;&#x2F;p&gt;
&lt;p&gt;In this post, I will go over what I call the &quot;bootstrap step.&quot;
How do you start with a fresh install of an OS, as easily as possible without having to &quot;manually&quot; install it from a USB stick on each machine?
The solution is to create a pre-configured NVMe drive, from a cloud OS image, and set up &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloud-init.io&#x2F;&quot;&gt;cloud init&lt;&#x2F;a&gt; to do the configuration that we need.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;theory-of-operation&quot;&gt;Theory of Operation&lt;a class=&quot;post-anchor&quot; href=&quot;#theory-of-operation&quot; aria-label=&quot;Anchor link for: theory-of-operation&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;blockquote class=&quot;markdown-alert-note&quot;&gt;
&lt;p&gt;I am running this process on macOS.
This comes with a few extra complications, compared to running this from a Linux machine&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
Should this be run from Linux, as it makes life easier?
Probably, but where&#x27;s the fun in that.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Before I dive into the details, let&#x27;s look at what needs to be done:&lt;&#x2F;p&gt;
&lt;p&gt;First, we need to write the image on the NVMe drive.
That&#x27;s easy enough.
However, I want the machines to be headless, so I don&#x27;t have any inputs or output connected.
Ideally when they boot, they are accessible from the network, and ready to set up.
This is where cloud-init comes in.&lt;&#x2F;p&gt;
&lt;p&gt;Cloud-init runs on a cloud VM during first boot, to handle machine-specific configuration.
That&#x27;s our exact use case!
It reads its configuration from a fairly straightforward &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.cloud-init.io&#x2F;en&#x2F;latest&#x2F;explanation&#x2F;format&#x2F;cloud-config.html&quot;&gt;yaml configuration format&lt;&#x2F;a&gt;.
While we aren&#x27;t running in &quot;the cloud,&quot; the abstraction makes sense, and cloud-init is flexible enough to work on a bare-metal machine.&lt;&#x2F;p&gt;
&lt;p&gt;The goal of the setup is to do 2 things:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Set up SSH key for remote access&lt;&#x2F;li&gt;
&lt;li&gt;Set up a hostname, so we can easily find it on the network using DNS.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;step-1-writing-the-image&quot;&gt;Step 1: Writing the image&lt;a class=&quot;post-anchor&quot; href=&quot;#step-1-writing-the-image&quot; aria-label=&quot;Anchor link for: step-1-writing-the-image&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;First, we need to choose an OS that we want to run.
For my setup I chose to run Ubuntu 25.10, the latest at the time of writing.
You can find releases on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloud-images.ubuntu.com&#x2F;releases&#x2F;&quot;&gt;this page.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Once we have that, we can&#x27;t write that directly to the NVMe drive.
They are in the QEMU Qcow2 image format (they are designed for cloud use).
We&#x27;ll need to convert them to raw.
We&#x27;ll do this with qemu&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-2-1&quot;&gt;&lt;a href=&quot;#fn-2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;qemu-img&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; convert&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; qcow2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; -O&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; raw ubuntu-25.10-server-cloudimg-amd64.img ubuntu-25.10-server-cloudimg-amd64.raw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we have an image we can write to the NVMe drive.
We&#x27;ll use &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.ansible.com&#x2F;&quot;&gt;Ansible&lt;&#x2F;a&gt; to automate the whole process, so it&#x27;s easily repeatable.&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;markdown-alert-caution&quot;&gt;
&lt;p&gt;Doing any of these following steps on the wrong disk WILL wipe out that disk, and you will lose any and all stored data!
Double check all paths, and your assumptions!
Proceed at your own risk!&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;We&#x27;ll assume that the following variables are defined:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;target_disk: &#x2F;dev&#x2F;disk2&lt;&#x2F;code&gt; - the buffered block device that our NVMe is mounted to&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_raw_disk: &#x2F;dev&#x2F;rdisk2&lt;&#x2F;code&gt; - the unbuffered block device for faster writes using &lt;code&gt;dd&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;First, we need to make sure that the drive is unmounted and ready to be written to:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Unmount disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil unmountDisk {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then we can copy the image to the NVMe drive:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write image to disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; dd if={{ image_src }} of={{ target_raw_disk }} bs=4m&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We now have an NVMe drive that will boot on our machine.
However, it doesn&#x27;t have the configuration needed for headless remote access.
So we&#x27;ll have to tackle that next, and this is where cloud-init comes into play.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;step-2-cloud-init-configuration&quot;&gt;Step 2: Cloud-init configuration&lt;a class=&quot;post-anchor&quot; href=&quot;#step-2-cloud-init-configuration&quot; aria-label=&quot;Anchor link for: step-2-cloud-init-configuration&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;Cloud-init has different ways in which it can get its initial scripts.
On a cloud machine, it&#x27;s usually through some kind of metadata service.
For example, on AWS it&#x27;s from &lt;code&gt;http:&#x2F;&#x2F;169.254.169.254&#x2F;latest&#x2F;&lt;&#x2F;code&gt;.
However, this isn&#x27;t a cloud machine, so we&#x27;ll have to do something different.&lt;&#x2F;p&gt;
&lt;p&gt;Luckily, Cloud-init supports the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.cloud-init.io&#x2F;en&#x2F;22.1&#x2F;topics&#x2F;datasources&#x2F;nocloud.html&quot;&gt;&lt;code&gt;NoCloud&lt;&#x2F;code&gt; datasource&lt;&#x2F;a&gt;.
This allows us to get the user data from a specific partition called &lt;code&gt;cidata&lt;&#x2F;code&gt; or &lt;code&gt;CIDATA&lt;&#x2F;code&gt;.
This is the option we&#x27;ll go with next&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-3-1&quot;&gt;&lt;a href=&quot;#fn-3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;creating-the-partition&quot;&gt;Creating the partition&lt;a class=&quot;post-anchor&quot; href=&quot;#creating-the-partition&quot; aria-label=&quot;Anchor link for: creating-the-partition&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Since I&#x27;m managing a Linux file system, I decided to use &lt;code&gt;gptfdisk&lt;&#x2F;code&gt;&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-4-1&quot;&gt;&lt;a href=&quot;#fn-4&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; for all the partition management.
First, we need to make sure that we see the whole disk, otherwise we&#x27;ll have no new space to create the new partition.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Make GPT see the full disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; sgdisk --move-second-header {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The default root (&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;) partition will expand to fill all the available space, so we don&#x27;t want to create our new partition immediately after.
This would prevent the partition from growing.
So we&#x27;ll have to create the partition at the end.
For this example, I chose a size of 50MB.
This is probably overkill for the 2 tiny files we&#x27;re writing, but it gives room to grow if we need to.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;Add CIDATA partition at the end&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; sgdisk --new=0:-51M:+50M --typecode=0:0700 --change-name=0:CIDATA {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next, we&#x27;ll format it as FAT32, and mount it:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;Format CIDATA to FAT32&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil eraseVolume MS-DOS CIDATA {{ target_disk }}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Mount CIDATA partition&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil mount {{ target_disk }}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At the end, if you run &lt;code&gt;diskutil&lt;&#x2F;code&gt;, you&#x27;ll get something like this, if all went well.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;dev&#x2F;disk2 (external, physical):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   #:                       TYPE NAME                    SIZE       IDENTIFIER&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   0:      GUID_partition_scheme                        *256.1 GB   disk2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   1: BC13C2FF-59E6-4262-A352-B275FD6F7172               1.1 GB     disk2s13&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   2:        Bios Boot Partition                         4.2 MB     disk2s14&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   3:                        EFI UEFI                    111.1 MB   disk2s15&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   4:           Linux Filesystem                         254.8 GB   disk2s1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   5:       Microsoft Basic Data CIDATA                  52.8 MB    disk2s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;writing-the-cloud-init-config&quot;&gt;Writing the cloud-init config&lt;a class=&quot;post-anchor&quot; href=&quot;#writing-the-cloud-init-config&quot; aria-label=&quot;Anchor link for: writing-the-cloud-init-config&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now, we&#x27;re ready to write our cloud-init files.
We&#x27;ll need two of them:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;meta-data&lt;&#x2F;code&gt; for setting the hostname&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;user-data&lt;&#x2F;code&gt; for setting up SSH access.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write cloud-init meta-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.copy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    content&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      local-hostname: {{ target_hostname }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    dest&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;Volumes&#x2F;CIDATA&#x2F;meta-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write cloud-init user-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.copy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    content&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      #cloud-config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      users:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      - name: ubuntu&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;        ssh_authorized_keys:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;        {% for key in public_keys -%}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          - {{ key }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;        {% endfor -%}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          sudo: ALL=(ALL) NOPASSWD:ALL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          shell: &#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    dest&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;Volumes&#x2F;CIDATA&#x2F;user-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  become&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We&#x27;ll unmount, eject, and we&#x27;re done.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Unmount disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil unmountDisk {{ target_disk }}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Eject disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil eject {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To create our new configured boot disk, all we need to do is run:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;ansible-playbook&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; playbooks&#x2F;ubuntu-start.yml&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --ask-become-pass -e&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;target_hostname=machine1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This makes setting up multiple machines for the same cluster really easy, as we only need to change the &lt;code&gt;target_hostname&lt;&#x2F;code&gt; variable when running the playbook.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;In this post, I&#x27;ve detailed how to create a bootable drive for a new PC, that will boot up, and configure itself with the correct hostname and SSH keys for remote access.
Using cloud-init allows us to specify a custom bootstrap step for each machine, so we can use the same &quot;base&quot; image in all cases.
This has greatly simplified adding new machines to the cluster, or reprovisioning old ones if we replace the SSD.&lt;&#x2F;p&gt;
&lt;p&gt;To state the obvious, this would be a lot simpler if done using Linux, as we could mount the root file system, and make our changes there.
In that case, using cloud-init is probably superfluous, and this post would be 1&#x2F;4 of the size it is now.
However, using cloud-init gives us more flexibility on what&#x27;s run.
More importantly, it presented a great opportunity for experimenting with a novel (to me) way of bootstrapping Linux on a bare-metal headless machine.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;appendix&quot;&gt;Appendix&lt;a class=&quot;post-anchor&quot; href=&quot;#appendix&quot; aria-label=&quot;Anchor link for: appendix&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;The full playbook:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Flash Ubuntu disk image&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  hosts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; localhost&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  become&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  vars&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    image_src&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;tmp&#x2F;ubuntu-25.10-server-cloudimg-amd64.raw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    target_raw_disk&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;dev&#x2F;rdisk2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    target_disk&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;dev&#x2F;disk2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    target_hostname&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ubuntu&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    public_keys&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ssh-rsa AAAAB3N....&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  tasks&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Check running on macOS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.assert&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        that&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ansible_facts[&amp;#39;os_family&amp;#39;] == &amp;quot;Darwin&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        fail_msg&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;This playbook can only be run on macOS&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Unmount disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil unmountDisk {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write image to disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; dd if={{ image_src }} of={{ target_raw_disk }} bs=4m&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Make GPT see the full disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; sgdisk --move-second-header {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;Add CIDATA partition at the end&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; sgdisk --new=0:-51M:+50M --typecode=0:0700 --change-name=0:CIDATA {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;Format CIDATA to FAT32&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil eraseVolume MS-DOS CIDATA {{ target_disk }}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Mount CIDATA partition&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil mount {{ target_disk}}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write cloud-init meta-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.copy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        content&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          local-hostname: {{ target_hostname }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        dest&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;Volumes&#x2F;CIDATA&#x2F;meta-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Write cloud-init user-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.copy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        content&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          #cloud-config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;          users:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;            - name: ubuntu&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;              ssh_authorized_keys:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;              {% for key in public_keys -%}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;                - {{ key }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;              {% endfor -%}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;              sudo: ALL=(ALL) NOPASSWD:ALL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;              shell: &#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        dest&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &#x2F;Volumes&#x2F;CIDATA&#x2F;user-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      become&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Unmount disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil unmountDisk {{ target_disk }}s2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Eject disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; diskutil eject {{ target_disk }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h1 id=&quot;errata&quot;&gt;Errata&lt;a class=&quot;post-anchor&quot; href=&quot;#errata&quot; aria-label=&quot;Anchor link for: errata&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026.06.09&lt;&#x2F;strong&gt; - Updated Ansible snippet and appendix playbook to fix hard-coded reference to &lt;code&gt;&#x2F;dev&#x2F;disk2&lt;&#x2F;code&gt; in the &quot;Mount CIDATA partition&quot; task.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;You &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.jeffgeerling.com&#x2F;blog&#x2F;2024&#x2F;mounting-ext4-linux-usb-drive-on-macos-2024&#x2F;&quot;&gt;can&#x27;t reliably mount an ext4 file system&lt;&#x2F;a&gt;, and using cloud-init gets around this limitation. &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;On a Mac, you can install the right package with &lt;code&gt;brew install qemu&lt;&#x2F;code&gt; if you&#x27;re using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;brew.sh&#x2F;&quot;&gt;Homebrew&lt;&#x2F;a&gt;. On a Debian-based Linux distro, you can use &lt;code&gt;apt install qemu-utils&lt;&#x2F;code&gt;. &lt;a href=&quot;#fr-2-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-3&quot;&gt;
&lt;p&gt;There&#x27;s also the option of passing in kernel arguments, and using a service on the local network. However, since mounting &lt;code&gt;ext4&lt;&#x2F;code&gt; partitions is not a feasible option, I&#x27;ve decided against this approach. &lt;a href=&quot;#fr-3-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-4&quot;&gt;
&lt;p&gt;You can install it with &lt;code&gt;brew install gptfdisk&lt;&#x2F;code&gt; &lt;a href=&quot;#fr-4-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</description>
      </item>
      <item>
          <title>Why I Self Host</title>
          <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/why-i-self-host/</link>
          <guid>https://caius.dev/blog/why-i-self-host/</guid>
          <description xml:base="https://caius.dev/blog/why-i-self-host/">&lt;p&gt;I have been running my own servers, on and off since college.
They have been a great experimentation platform, and given me resources to tap into for my professional life.
It wasn&#x27;t always smooth sailing, but here&#x27;s some of my takes, and why it&#x27;s always been worth the hassle, at least for me.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;a-short-history&quot;&gt;A short history&lt;a class=&quot;post-anchor&quot; href=&quot;#a-short-history&quot; aria-label=&quot;Anchor link for: a-short-history&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;the-early-days&quot;&gt;The early days&lt;a class=&quot;post-anchor&quot; href=&quot;#the-early-days&quot; aria-label=&quot;Anchor link for: the-early-days&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The first  &quot;server&quot; I built was during college when I took an old desktop, put Linux on it, and used it mostly as a network file store.
No redundancy, but it was fun.&lt;&#x2F;p&gt;
&lt;p&gt;Later on during grad school, I had to setup the research group server&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
Using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;account.dyn.com&#x2F;&quot;&gt;DynDNS&lt;&#x2F;a&gt;&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-2-1&quot;&gt;&lt;a href=&quot;#fn-2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, it was publicly accessible.
That worked fine for a while, until a vulnerable Apache module turned it into a spam bot.
I only found out when I got an annoyed email from a sysadmin in Bucharest, Romania, that it was spamming their network.
Took it down for the day, cleaned it up, and made a note to apply patches more often in the future.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;today&quot;&gt;Today&lt;a class=&quot;post-anchor&quot; href=&quot;#today&quot; aria-label=&quot;Anchor link for: today&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Currently, I am running a Pi Cluster with 4 Raspberry Pi 4 Nodes, with 4 GB each, running &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;k3s.io&#x2F;&quot;&gt;K3s&lt;&#x2F;a&gt;.
While not a very powerful cluster, and only using SD Card for storage, it&#x27;s enough to serve a static website, grafana, prometheus, and any random experiments.
I also have a 1 node Framework Desktop that will run more &quot;critical&quot; systems, like a Postgres database for services that need it.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;why&quot;&gt;Why?&lt;a class=&quot;post-anchor&quot; href=&quot;#why&quot; aria-label=&quot;Anchor link for: why&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;A lot of the services I run (or plan to) are usually available for free (or a reasonable subscription) else where.
You can host a website cheaply on AWS with S3 and Cloudfront (assuming low traffic).
You can host your 3D prints on Printables and the like, and I do.
But there are a few advantages to having control of where your data ends up.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;learning&quot;&gt;Learning&lt;a class=&quot;post-anchor&quot; href=&quot;#learning&quot; aria-label=&quot;Anchor link for: learning&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This has been the primary reason for this. The Pi Cluster in particular was a great learning experience. Testing a repeatable Ansible set of playbooks, getting K3S to work and deploying a few services using both plain Kubernetes templates and Helm is not something I get to do in my day job.&lt;&#x2F;p&gt;
&lt;p&gt;I find that I learn best when working on something practical. This is true from programming languages to infrastructure and networking. Having a &quot;disposable&quot; setup makes this a lot easier when learning.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;enshittification&quot;&gt;Enshittification&lt;a class=&quot;post-anchor&quot; href=&quot;#enshittification&quot; aria-label=&quot;Anchor link for: enshittification&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s an unfortunate reality of today&#x27;s world.
Free services get more restrictive, open source alternatives get yanked&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-3-1&quot;&gt;&lt;a href=&quot;#fn-3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, or the prices get constantly increased, with &quot;AI&quot; as the excuse&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-4-1&quot;&gt;&lt;a href=&quot;#fn-4&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Having your own services protects from this reality, and you don&#x27;t need to worry as much about increased costs, lost access, etc.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;private-access&quot;&gt;Private access&lt;a class=&quot;post-anchor&quot; href=&quot;#private-access&quot; aria-label=&quot;Anchor link for: private-access&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;From the grad school days, I&#x27;ve learned to be wary when exposing anything over the internet.
To this end, the cluster is currently private and I access it from anywhere using Tailscale.
The free tier&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-5-1&quot;&gt;&lt;a href=&quot;#fn-5&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; is enough for me and my partner, and we can access the services from anywhere.
Of course, locally we can always use the local network.&lt;&#x2F;p&gt;
&lt;p&gt;Using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cert-manager.io&#x2F;&quot;&gt;Cert Manager&lt;&#x2F;a&gt;,
I have publicly valid certificates and DNS that points to the Tailscale IP address of a node in the cluster.
This works great, and we don&#x27;t get any annoying browser warnings.
Making everything public is only a matter of updating the DNS records.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;your-data-your-rules&quot;&gt;Your data, your rules&lt;a class=&quot;post-anchor&quot; href=&quot;#your-data-your-rules&quot; aria-label=&quot;Anchor link for: your-data-your-rules&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Keeping the data private means it&#x27;s not going to be used to train AI models, and target ads at you.
I&#x27;d rather have my data truly private, not private with an asterisk and a disclaimer, as the service can still read it but promises to play nice.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-downsides&quot;&gt;The downsides&lt;a class=&quot;post-anchor&quot; href=&quot;#the-downsides&quot; aria-label=&quot;Anchor link for: the-downsides&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;reliability&quot;&gt;Reliability&lt;a class=&quot;post-anchor&quot; href=&quot;#reliability&quot; aria-label=&quot;Anchor link for: reliability&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;My setup is only as reliable as my internet and power are.
A UPS helps with the power, but no backup to the internet connection.
If Comcast is being its usual self, we&#x27;ll get a few periods of downtime a week.
For now this is not an issue, as the total downtime is less than 10 minutes a week, and mostly at night.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;data-safety&quot;&gt;Data safety&lt;a class=&quot;post-anchor&quot; href=&quot;#data-safety&quot; aria-label=&quot;Anchor link for: data-safety&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If anything were to happen to the hardware, well, the data is gone.
That is solved by having automatic backups to S3&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-6-1&quot;&gt;&lt;a href=&quot;#fn-6&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, and having a good &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.backblaze.com&#x2F;blog&#x2F;the-3-2-1-backup-strategy&#x2F;&quot;&gt;3-2-1 backup policy&lt;&#x2F;a&gt; is critical.&lt;&#x2F;p&gt;
&lt;p&gt;Also, test your backups regularly.
You don&#x27;t want to find that your backups are broken when you really need them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cost&quot;&gt;Cost&lt;a class=&quot;post-anchor&quot; href=&quot;#cost&quot; aria-label=&quot;Anchor link for: cost&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In particular, the upfront costs of getting the hardware.
This is only getting more expensive these days&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-7-1&quot;&gt;&lt;a href=&quot;#fn-7&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
However, if you&#x27;ve got some old hardware lying around, or go the used option, it&#x27;s going to be less of a concern.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;security&quot;&gt;Security&lt;a class=&quot;post-anchor&quot; href=&quot;#security&quot; aria-label=&quot;Anchor link for: security&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If the spambot incident taught me anything, it&#x27;s that if it&#x27;s open to the internet it&#x27;s a target.
Data leaks could be more serious if it has private data, and you&#x27;re the only one in charge and responsible for securing it.
Keeping up to date with patches becomes even more critical the more sensitive the stored data.
A solution like Tailscale sidesteps the issue by keeping everything private.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;Is this for everyone?
No. But if you want to learn, or care about where data ends up living, this is something worth looking into.&lt;&#x2F;p&gt;
&lt;!-- Footnotes --&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;In our living room, because the university policies for a &quot;hard-wired&quot; server were beyond draconian, and made the hardware useless for the research we were doing &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;It was free in those days &lt;a href=&quot;#fr-2-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-3&quot;&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;selfhosted&#x2F;comments&#x2F;1r3zkg7&#x2F;minio_github_repository_officially_archived&#x2F;&quot;&gt;Minio archiving their open source repo&lt;&#x2F;a&gt; &lt;a href=&quot;#fr-3-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-4&quot;&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=44225782&quot;&gt;Google Workspace price increase due to new AI features&lt;&#x2F;a&gt; &lt;a href=&quot;#fr-4-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-5&quot;&gt;
&lt;p&gt;I am aware that this could enshittify in the future. However, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;headscale.net&#x2F;stable&#x2F;&quot;&gt;Headscale&lt;&#x2F;a&gt; is an option, so I&#x27;m confident there&#x27;s an offramp if I need it &lt;a href=&quot;#fr-5-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-6&quot;&gt;
&lt;p&gt;For data durability, I haven&#x27;t found anything that beats S3, or other key store offerings by a large cloud vendor. &lt;a href=&quot;#fr-6-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-7&quot;&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.theverge.com&#x2F;news&#x2F;839353&#x2F;pc-ram-shortage-pricing-spike-news&quot;&gt;RAM price hikes&lt;&#x2F;a&gt; &lt;a href=&quot;#fr-7-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</description>
      </item>
      <item>
          <title>Self Hosted Postgres in Kubernetes with PITR Recovery</title>
          <pubDate>Wed, 31 Dec 2025 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/self-hosted-postgres/</link>
          <guid>https://caius.dev/blog/self-hosted-postgres/</guid>
          <description xml:base="https://caius.dev/blog/self-hosted-postgres/">&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;a class=&quot;post-anchor&quot; href=&quot;#introduction&quot; aria-label=&quot;Anchor link for: introduction&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;I would like to selfhost a few services, and they require persistence, in the form of a PostgreSQL database.
Since my Kubernetes cluster runs on 4 Raspberry Pi 4, each with an SD card for disk, it&#x27;s only a matter of time until one of them gets corrupted.
To avoid the innevitable and predictable data loss, I&#x27;ll need automated backups, idealy with Point-In-Time Recovery (PITR).&lt;&#x2F;p&gt;
&lt;p&gt;If I&#x27;m going to reinvent the wheel, why not go for the whole wheel?&lt;&#x2F;p&gt;
&lt;h1 id=&quot;solution&quot;&gt;Solution&lt;a class=&quot;post-anchor&quot; href=&quot;#solution&quot; aria-label=&quot;Anchor link for: solution&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudnative-pg.io&#x2F;documentation&#x2F;current&#x2F;&quot;&gt;CloudNativePG&lt;&#x2F;a&gt; seems to be most mature and feature complete out there.
It provides an operator that does a lot of the heavy lifting.
Backups are handled by the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudnative-pg.io&#x2F;plugin-barman-cloud&#x2F;&quot;&gt;Barman Cloud&lt;&#x2F;a&gt; plugin.
It handles aspects like WAL Log archiving, taking regular snapshots and uploading them a cloud object storage, like S3&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
Seems to meet all the needs I need for my project.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;setup&quot;&gt;Setup&lt;a class=&quot;post-anchor&quot; href=&quot;#setup&quot; aria-label=&quot;Anchor link for: setup&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;setting-up-a-cloud-native-pg-database&quot;&gt;Setting up a Cloud Native PG database&lt;a class=&quot;post-anchor&quot; href=&quot;#setting-up-a-cloud-native-pg-database&quot; aria-label=&quot;Anchor link for: setting-up-a-cloud-native-pg-database&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Using the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudnative-pg.io&#x2F;docs&#x2F;1.28&#x2F;installation_upgrade&quot;&gt;instructions on their website&lt;&#x2F;a&gt;, we&#x27;ll need to install the operator.
We can do it with the following command:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubectl apply&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --server-side -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;cloudnative-pg&#x2F;cloudnative-pg&#x2F;release-1.28&#x2F;releases&#x2F;cnpg-1.28.0.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can now create our database.
The Barman Cloud Plugin we&#x27;ll be using later assumes that the DB is in the &lt;code&gt;cnpg-system&lt;&#x2F;code&gt; namespace.
To simply this experiment, we&#x27;ll work with that assumption, and we&#x27;ll start by creating the namespace:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubectl create namespace cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then we can define our cluster configuration, with one user so we can connect to it later:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubernetes.io&#x2F;basic-auth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; postgres-secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;stringData&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  username&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; user1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  password&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; supersecretpassword&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;---&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; postgresql.cnpg.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Cluster&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cpgn-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  instances&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  storage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    size&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; 1Gi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And when we apply this, we have a new cluster with 1 instance.
We can forward the port, and connect to it like we normally would to a Postgres instance running locally.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubectl port-forward&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; -n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system service&#x2F;test-restore-rw 5432:5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;backups&quot;&gt;Backups&lt;a class=&quot;post-anchor&quot; href=&quot;#backups&quot; aria-label=&quot;Anchor link for: backups&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;installing&quot;&gt;Installing&lt;a class=&quot;post-anchor&quot; href=&quot;#installing&quot; aria-label=&quot;Anchor link for: installing&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Now, we can configure backups for our new Postgres cluster.&lt;&#x2F;p&gt;
&lt;p&gt;First, let&#x27;s install the prerequisites that the Barman Cloud plugin requires.
We will need to install Certificate Manager&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-2-1&quot;&gt;&lt;a href=&quot;#fn-2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubectl apply&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;github.com&#x2F;cert-manager&#x2F;cert-manager&#x2F;releases&#x2F;download&#x2F;v1.19.2&#x2F;cert-manager.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then, we can install the plugin:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; kubectl apply&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; https:&#x2F;&#x2F;github.com&#x2F;cloudnative-pg&#x2F;plugin-barman-cloud&#x2F;releases&#x2F;download&#x2F;v0.9.0&#x2F;manifest.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;configuration&quot;&gt;Configuration&lt;a class=&quot;post-anchor&quot; href=&quot;#configuration&quot; aria-label=&quot;Anchor link for: configuration&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;We can now define the backup configuration.
For this example, I&#x27;ll use S3 as the storage target for the backups.&lt;&#x2F;p&gt;
&lt;p&gt;First, we&#x27;ll need to store a set AWS credentials with access to the right bucket&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-3-1&quot;&gt;&lt;a href=&quot;#fn-3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.
We&#x27;ll use an &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kubernetes.io&#x2F;docs&#x2F;concepts&#x2F;configuration&#x2F;secret&#x2F;#secret-types&quot;&gt;Opaque Secret&lt;&#x2F;a&gt; to store the access and secret keys:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Opaque&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;stringData&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  access-key-id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;lt;redacted&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  secret-key-id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;lt;redacted&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next, we&#x27;ll define the storage configuration to our S3 Bucket:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; barmancloud.cnpg.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ObjectStore&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  configuration&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    destinationPath&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3:&#x2F;&#x2F;&amp;lt;bucket&amp;gt;&#x2F;postgres&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    s3Credentials&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      accessKeyId&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; access-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      secretAccessKey&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-credentials&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        key&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; secret-key-id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    wal&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      compression&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; gzip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, we&#x27;ll need to tell our Postgres cluster to use this configuration, and enable WAL archiving.
We&#x27;ll add the following to the Cluster &lt;code&gt;spec&lt;&#x2F;code&gt; field:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;plugins&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; barman-cloud.cloudnative-pg.io&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    isWALArchiver&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    parameters&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      barmanObjectName&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The final piece of the puzzle is setup regular &quot;base&quot; backups.
These will backup the entire dataset, and give us a &quot;base&quot; for the WAL logs to be applied to in order to get our Point-In-Time Restore.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; postgresql.cnpg.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; ScheduledBackup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; pg-backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  cluster&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  schedule&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;0 0 * * * *&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt; # hourly&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  backupOwnerReference&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; self&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  method&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; plugin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  pluginConfiguration&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; barman-cloud.cloudnative-pg.io&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;results&quot;&gt;Results&lt;a class=&quot;post-anchor&quot; href=&quot;#results&quot; aria-label=&quot;Anchor link for: results&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Now that everything is configured, we can list our S3 bucket, and sure enough, we have backups.
The top level structure has our base and WAL logs:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ aws s3 ls s3:&#x2F;&#x2F;&amp;lt;redacted&amp;gt;&#x2F;postgres&#x2F;test&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE base&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE wals&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Digging in deeper, we have our base backups, nicely named by timestamp, one every hour, as we&#x27;d expect:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ aws s3 ls s3:&#x2F;&#x2F;&amp;lt;redacted&amp;gt;&#x2F;postgres&#x2F;test&#x2F;base&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE 20251227T030000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE 20251227T040000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE 20251227T050000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE 20251227T060000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           PRE 20251227T070000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And finally, we have our WAL logs archived:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ aws s3 ls s3:&#x2F;&#x2F;&amp;lt;redacted&amp;gt;&#x2F;postgres&#x2F;test&#x2F;wals&#x2F;0000000100000000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 21:00:02      16944 000000010000000000000013.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 21:00:04        210 000000010000000000000014.00000028.backup.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 21:00:03      16513 000000010000000000000014.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 21:05:03      17139 000000010000000000000015.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 21:30:03      17276 000000010000000000000016.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 22:00:02      16416 000000010000000000000017.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 22:00:05        209 000000010000000000000018.00000028.backup.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2025-12-26 22:00:04      16510 000000010000000000000018.gz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So we should have all the pieces needed to perform a PITR.
We&#x27;ll tacke this in the next section.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;restoring&quot;&gt;Restoring&lt;a class=&quot;post-anchor&quot; href=&quot;#restoring&quot; aria-label=&quot;Anchor link for: restoring&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For testing the restore, I&#x27;ve created a simple table, with timestamps for easy reasoning:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; test&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #66D9EF;font-style: italic;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #66D9EF;font-style: italic;&quot;&gt;timestamp&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;6&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And we inserted different values, and this is the end state of the table:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;test=&amp;gt; select * from test;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; id |         created_at         &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;----+----------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 16 | 2025-12-27 03:00:17.601392&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 17 | 2025-12-27 03:00:20.438822&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 18 | 2025-12-27 03:28:06.64914&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 19 | 2025-12-27 20:42:25.563207&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 20 | 2025-12-27 20:42:32.992066&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 21 | 2025-12-27 20:45:58.634257&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(6 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We&#x27;ll restore the table to 2025-12-27, at 20:43:00 UTC.
For this, we&#x27;ll create a new cluster, and we&#x27;ll point it at the backups we have.
We&#x27;ll also need to give it the target time we want the cluster restored to.
We arrive at this configuration:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;apiVersion&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; postgresql.cnpg.io&#x2F;v1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;kind&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; Cluster&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; test-restore&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  namespace&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; cnpg-system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;spec&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  instances&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  imagePullPolicy&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; IfNotPresent&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  bootstrap&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    recovery&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      source&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; source&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  externalClusters&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; source&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    plugin&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; barman-cloud.cloudnative-pg.io&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;      parameters&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        barmanObjectName&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; s3-store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        serverName&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;        targetTime&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;2025-12-27T20:43:00Z&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;  storage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    size&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; 1Gi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Once the restore is done (it was pretty much instant for this example), we con connect and check our test table:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;test=&amp;gt; select * from test;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; id |         created_at         &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;----+----------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 16 | 2025-12-27 03:00:17.601392&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 17 | 2025-12-27 03:00:20.438822&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 18 | 2025-12-27 03:28:06.64914&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 19 | 2025-12-27 20:42:25.563207&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 20 | 2025-12-27 20:42:32.992066&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(5 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As we&#x27;d expect, we&#x27;re missing row with &lt;code&gt;id&lt;&#x2F;code&gt; 21, as it&#x27;s &lt;em&gt;after&lt;&#x2F;em&gt; the target restore time.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;conclusions-and-final-remarks&quot;&gt;Conclusions and Final Remarks&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusions-and-final-remarks&quot; aria-label=&quot;Anchor link for: conclusions-and-final-remarks&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h1&gt;
&lt;p&gt;All in all, this was an easy setup, and it works fine for at least the basic use case.
The true test is once this sees some &quot;production&quot; loads, and testing with an actual live data base.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-note-on-s3-cots&quot;&gt;A note on S3 cots&lt;a class=&quot;post-anchor&quot; href=&quot;#a-note-on-s3-cots&quot; aria-label=&quot;Anchor link for: a-note-on-s3-cots&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For this example, I used S3 as the backup target destination.
The amount of data stored is small, however, WAL archiving could end up writing a lot of objects.
This could incur significant S3 API charges, so it&#x27;s something I&#x27;m keeping an eye on.
With hourly base backups, I didn&#x27;t see any cost increases for my AWS account.
But inserting 21 records, and deleting a few is not exactly a representative use case, but at least the &quot;baseline&quot; cost is not absurd.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;retention-policies&quot;&gt;Retention policies&lt;a class=&quot;post-anchor&quot; href=&quot;#retention-policies&quot; aria-label=&quot;Anchor link for: retention-policies&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Barman has &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudnative-pg.io&#x2F;plugin-barman-cloud&#x2F;docs&#x2F;retention&#x2F;&quot;&gt;the option of specifying retention policies&lt;&#x2F;a&gt;.
However, for this experiment, I&#x27;ve gone with specifying a lifecycle policy on the S3 bucket.
Everything under &lt;code&gt;postgres&#x2F;&lt;&#x2F;code&gt; will be deleted after 7 days.
This will give me a one week recovery window.&lt;&#x2F;p&gt;
&lt;p&gt;Using the Barman retention policy will cause the plugin to list S3, and then delete the objects.
This also will incur some S3 API charges, and I think that using the S3 lifecycle rule is probably good enough.&lt;&#x2F;p&gt;
&lt;!-- Footnotes --&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;The plugin also &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudnative-pg.io&#x2F;plugin-barman-cloud&#x2F;docs&#x2F;object_stores&#x2F;&quot;&gt;supports Google Cloud Storage, or Azure Blobs or some other services that implementation a compatible API&lt;&#x2F;a&gt;. &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;You can skip this step if your cluster already has it installed on the cluster. My test cluster did not. &lt;a href=&quot;#fr-2-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-3&quot;&gt;
&lt;p&gt;For reference, I granted the user full access to the S3 bucket where the backups are stored. &lt;a href=&quot;#fr-3-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</description>
      </item>
      <item>
          <title>Setting up the DX Commander Expedition Antenna</title>
          <pubDate>Mon, 21 Apr 2025 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/dx-commander-expedition/</link>
          <guid>https://caius.dev/blog/dx-commander-expedition/</guid>
          <description xml:base="https://caius.dev/blog/dx-commander-expedition/">&lt;p&gt;Recently I got a new &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dxcommander.com&#x2F;product&#x2F;dx-commander-expedition-kit&#x2F;&quot;&gt;DXCommander Expedition Antenna&lt;&#x2F;a&gt; for Parks on the Air (POTA) use.
After some experimentations, this week I decided to finish the tuning, and make a few contacts with it&lt;&#x2F;p&gt;
&lt;h2 id=&quot;initial-thoughts&quot;&gt;Initial thoughts&lt;a class=&quot;post-anchor&quot; href=&quot;#initial-thoughts&quot; aria-label=&quot;Anchor link for: initial-thoughts&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s a large antenna!
See in context with a large pine tree.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;antenna.012761669250aaa4.jpg&quot; alt=&quot;DX Commander Expedion Antenna&quot;
     width=&quot;960&quot; height=&quot;1280&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;antenna.5101a32058764802.jpg 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;antenna.60997bb223dfa8d7.jpg 784w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;antenna.e1a51cba35fce9f3.jpg 1280w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;It took a while to set up, with a couple of saw horses it&#x27;s not too bad.
Having the antenna at a reasonble height avoids all be bending over to cut and measure things.
Once it was clear where everything went, and had the clamps trimmed to size, the assembly went smooth.&lt;&#x2F;p&gt;
&lt;p&gt;I also tried tryting to extend the antenna while it&#x27;s vertical.
I don&#x27;t recomment this approach.
It doesn&#x27;t work and it will come crashing down and &quot;colapse&quot; quite fast.&lt;&#x2F;p&gt;
&lt;p&gt;The best option for deploying in the field is to lay it down, assemble everything, then walk it up.
It&#x27;s light enough that it&#x27;s easy to do with a single person.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-good&quot;&gt;The good&lt;a class=&quot;post-anchor&quot; href=&quot;#the-good&quot; aria-label=&quot;Anchor link for: the-good&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The tuning! 40 and 20 meters are spot on, with barely any noticeable movement of the SWR meter while transmitting.&lt;&#x2F;p&gt;
&lt;p&gt;20 meters is well below 2.0 SWR across the entire band.
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;20m.d0ca9e0cd1179d11.png&quot; alt=&quot;20 meter tunning&quot;
     width=&quot;1031&quot; height=&quot;1279&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;20m.3df5bdbe485a607a.png 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;20m.9f1c05e82fb6ec9f.png 784w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;For 40 meters, the story is the same.
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;40m.2842b8ff69f588d9.png&quot; alt=&quot;40 meter tunning&quot;
     width=&quot;1031&quot; height=&quot;1279&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;40m.cb2c589b013a6f01.png 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;40m.709bed10188e2c1b.png 784w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;The 40 meters element ended up a lot longer than I expected.
So long, in fact, that I don&#x27;t need any shockcord at the top.
It reaches the top of the pole, and I use the little clear platic tubing to hold the element in place.
A loop of shockcork keeps me from loosing the little plastic tube.&lt;&#x2F;p&gt;
&lt;p&gt;I opted for a &quot;perfect&quot; 40 meter tune, and this probably lead to the long element, and the not so great 15 meter performance (more on that later).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-ok&quot;&gt;The OK&lt;a class=&quot;post-anchor&quot; href=&quot;#the-ok&quot; aria-label=&quot;Anchor link for: the-ok&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Tuning the 10 meter band was a bit more finicky than I liked.
I was able to get it under 3.0 across the whole band, and I was able to use it fine without using the internal ATU of the radio.&lt;&#x2F;p&gt;
&lt;p&gt;(Sorry, no image of the 10 meter turning.
I closed the software and didn&#x27;t save the image, and I only realized this after the antenna was down.
I&#x27;m not going to stand it back up, in the rain, for one image.
You&#x27;ll have to trust me on this one)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-meh&quot;&gt;The meh&lt;a class=&quot;post-anchor&quot; href=&quot;#the-meh&quot; aria-label=&quot;Anchor link for: the-meh&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;15 meters.
It&#x27;s tuning is not great, see below.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;15m.a8f544f9fde907b1.png&quot; alt=&quot;15 meter tunning&quot;
     width=&quot;1031&quot; height=&quot;1279&quot;
     sizes=&quot;(min-width: 920px) 784px, (min-width: 700px) calc(82vw + 46px), calc(100vw - 40px)&quot; 
     srcset=&quot;https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;15m.421ca08d39725446.png 640w,
             https:&#x2F;&#x2F;caius.dev&#x2F;processed_images&#x2F;15m.db281ada9a76f3fa.png 784w&quot;
     loading=&quot;lazy&quot;&gt;
&lt;p&gt;In practice, I get under 2.0 with the Icom 7300 (very brief testing).
It&#x27;s usable without an ATU, and well within ATU limits if I&#x27;m using a radio that has one built in.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s possible the &quot;overtunning&quot; the 40 meter band got me worse performance here.
But it&#x27;s a compromise I made on purpose.
I use 40 meters a lot more often than 15, and I&#x27;m happy with the overall result.&lt;&#x2F;p&gt;
&lt;p&gt;Also, the elements are a bit loose than I&#x27;d like.
The tensioning loops ended up very close to the spreader plates.
This might be my error in assembly, but for future reference, having the loops a few inches down is OK, as there&#x27;s pleny of shockcord to accomodate this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;a class=&quot;post-anchor&quot; href=&quot;#conclusions&quot; aria-label=&quot;Anchor link for: conclusions&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s a great antenna!&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s larger than I expected.
But it did deliver the performance I was expecting.
It&#x27;s now in the back of the truck, and I&#x27;m looking forward to trying out in the field on the next POTA activation!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Decoding Meshtastic Channel Links</title>
          <pubDate>Sat, 25 Jan 2025 00:00:00 +0000</pubDate>
          <author>Caius Brindescu</author>
          <link>https://caius.dev/blog/decoding-meshtastic-links/</link>
          <guid>https://caius.dev/blog/decoding-meshtastic-links/</guid>
          <description xml:base="https://caius.dev/blog/decoding-meshtastic-links/">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;a class=&quot;post-anchor&quot; href=&quot;#introduction&quot; aria-label=&quot;Anchor link for: introduction&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If you don&#x27;t know what Meshtastic is, this &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=N3FXej9fqIk&quot;&gt;video has a great introduction&lt;&#x2F;a&gt;.
The TL;DR: is that it&#x27;s a communication protocol using 900 MHz (in the US) &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;LoRa&quot;&gt;LORA Radios&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Communication is done either via direct messages (DMs), or via channels.
The channels are encrypted, so for 2 parties to communicate, they need to know the encryption key (pre-shared key or PSK, for short) for that channel.
Sharing the chaneel name, key etc. is done via &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;meshtastic.org&#x2F;e&#x2F;&quot;&gt;QR codes or URLs&lt;&#x2F;a&gt;.
In this blog post, we&#x27;ll look at what information is encoded when sharing channel settings, and we can decode it using a &quot;simple&quot; Python script.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-channel-link&quot;&gt;The channel link&lt;a class=&quot;post-anchor&quot; href=&quot;#the-channel-link&quot; aria-label=&quot;Anchor link for: the-channel-link&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We&#x27;ll use the following link as an example:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;meshtastic.org&#x2F;e&#x2F;#CjQSIOsfCkgIpGY_8iW02ad-4QPaCSBISJqzIVoZKHdqKXd8GgtCbG9nQ2hhbm5lbCUEAAAAEg4IATgBQANIAVAeWBRoAQ&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The part that we are interested in is the fragment:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CjQSIOsfCkgIpGY_8iW02ad-4QPaCSBISJqzIVoZKHdqKXd8GgtCbG9nQ2hhbm5lbCUEAAAAEg4IATgBQANIAVAeWBRoAQ&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Base64&quot;&gt;URL safe base64&lt;&#x2F;a&gt; encoded string that has the information we are looking for.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;structure&quot;&gt;Structure&lt;a class=&quot;post-anchor&quot; href=&quot;#structure&quot; aria-label=&quot;Anchor link for: structure&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The message is encoded using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;protobuf.dev&#x2F;&quot;&gt;protobuf&lt;&#x2F;a&gt;.
The structure we are interested in is defined &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;meshtastic&#x2F;protobufs&#x2F;blob&#x2F;2cffaf53e3faf1b6e41a8b8f05312f2f893be413&#x2F;meshtastic&#x2F;channel.proto&quot;&gt;in this file&lt;&#x2F;a&gt;.
It&#x27;s reasonably well documented, so I will not get into all the details here.
But the main properties we are interested in are the channel name, and the PSK.
They are encoded as the &lt;code&gt;psk&lt;&#x2F;code&gt; and &lt;code&gt;name&lt;&#x2F;code&gt; properties in the top level &lt;code&gt;ChannelSettings&lt;&#x2F;code&gt; message.&lt;&#x2F;p&gt;
&lt;p&gt;For the rest of this post, we&#x27;ll ignore the remainder of the properties, but they can be extracted using the same approach.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;protobuf-and-python&quot;&gt;Protobuf and Python&lt;a class=&quot;post-anchor&quot; href=&quot;#protobuf-and-python&quot; aria-label=&quot;Anchor link for: protobuf-and-python&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;generating-python-code-from-the-protobuf-structure&quot;&gt;Generating Python code from the protobuf structure&lt;a class=&quot;post-anchor&quot; href=&quot;#generating-python-code-from-the-protobuf-structure&quot; aria-label=&quot;Anchor link for: generating-python-code-from-the-protobuf-structure&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The first setup is installing all the prerequisites.
There are 2 parts: the protobuf compiler and the Python protobuf library.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; apt install protobuf-compiler&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;pip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; install protobuf==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;3.20.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: I recommend using a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;3&#x2F;library&#x2F;venv.html&quot;&gt;virtual enviroment&lt;&#x2F;a&gt; for your python setup.
Here&#x27;s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;packaging.python.org&#x2F;en&#x2F;latest&#x2F;guides&#x2F;installing-using-pip-and-virtual-environments&#x2F;#create-and-use-virtual-environments&quot;&gt;a good resource for setting it up&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Next, we&#x27;ll need to get the protobuf definition &quot;compiled&quot; into python code, so we can the deserialize the configuration from the URL.
The protobuf definition &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;meshtastic&#x2F;protobufs&quot;&gt;lives in GitHub&lt;&#x2F;a&gt;, so we&#x27;ll need to clone it.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; clone git@github.com:meshtastic&#x2F;protobufs.git&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We&#x27;ll need to compile a total of four proto files to get this working.
First, let&#x27;s create a new folder for our generated files to live in:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;mkdir&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then, &lt;code&gt;cd&lt;&#x2F;code&gt; into the &lt;code&gt;protobuf&lt;&#x2F;code&gt; folder, and run the following commands.
(You can compile all of them if you want to, but it&#x27;s not needed for this use case.)&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;protoc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --python_out=..&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&#x2F;apponly.proto&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;protoc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --python_out=..&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&#x2F;channel.proto&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;protoc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --python_out=..&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&#x2F;device_ui.proto&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;protoc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt; --python_out=..&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&#x2F;config.proto&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, we need to make the &lt;code&gt;meshtastic&lt;&#x2F;code&gt; folder a python &quot;module&quot;, we we&#x27;ll create an empty &lt;code&gt;__init__.py&lt;&#x2F;code&gt; file inside:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;touch&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; meshtastic&#x2F;__init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;decoding&quot;&gt;Decoding&lt;a class=&quot;post-anchor&quot; href=&quot;#decoding&quot; aria-label=&quot;Anchor link for: decoding&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;How that have all the prerequisites, let&#x27;s get decoding!&lt;&#x2F;p&gt;
&lt;p&gt;To start with we&#x27;ll need to import the required pacakges:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; meshtastic.apponly_pb2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ChannelSet&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; base64&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We&#x27;ll see the string we want to decode as a constant (for now):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;TO_DECODE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;CjESIMC70tNI5vkpQpHZGeg0WV7y6KqEoD0_t74fM1_jCaMkGghCbG9nVGVzdCUEAAAAEg4IATgBQANIAVAeWBRoAQ&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next, we&#x27;ll create a new, empty object to populate with the deserialized data:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;channelSet&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ChannelSet()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, let&#x27;s decode and display the channel name and PSK:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;channelSet.ParseFromString(base64.urlsafe_b64decode(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;TO_DECODE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;==&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; channelSet.settings:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #66D9EF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;&amp;quot;Channel name: &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; s.name)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #66D9EF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;&amp;quot;Psk: &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; base64.b64encode(s.psk).decode())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A few notes on the implementation.
The settings are encoded in a &quot;URL Safe&quot; base64 encoding.
However, Python still expects the padding to be there, so we&#x27;re adding the maximum of 2 padding characters to get around this.
If that&#x27;s too many, any extras will be ignored by the decoder.
&quot;Proper&quot; URL Safe encoding explictly omits padding, but the Python library still requires it, so here we are.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;a class=&quot;post-anchor&quot; href=&quot;#results&quot; aria-label=&quot;Anchor link for: results&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Running the above code, we&#x27;ll get the settings we expect:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ python decode.py &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Channel name: BlogTest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Psk: wLvS00jm+SlCkdkZ6DRZXvLoqoSgPT+3vh8zX+MJoyQ=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can extract other properties if we want to, using the same approach.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-end&quot;&gt;The End&lt;a class=&quot;post-anchor&quot; href=&quot;#the-end&quot; aria-label=&quot;Anchor link for: the-end&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
</description>
      </item>
    </channel>
</rss>
