<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Backups on bismigel</title><link>https://bismigel.dev/tags/backups/</link><description>Recent content in Backups on bismigel</description><generator>Hugo</generator><language>en-us</language><copyright>bismigel</copyright><lastBuildDate>Sat, 17 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://bismigel.dev/tags/backups/index.xml" rel="self" type="application/rss+xml"/><item><title>Hardening a homelab: backups, drift and silent failures</title><link>https://bismigel.dev/posts/2026-10-17-what-hardening-taught-me/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><guid>https://bismigel.dev/posts/2026-10-17-what-hardening-taught-me/</guid><description/><content:encoded><![CDATA[<p>I spent a day trying to make the homelab <strong>honest</strong>. I did not add features; I did the opposite. I went looking for the places where the lab was quietly lying to me, and wrote down what I found. Some of it was embarrassing.</p>
<h1 id="a-backup-you-have-not-restored-is-a-hope">A backup you have not restored is a hope</h1>
<p>The backups ran every night. For a while, they were also allowed to &ldquo;skip&rdquo; a component and still report success. The unit went green, the archive was incomplete, and nobody knew.</p>
<p>The fix was not exotic. If any dump is missing, the backup fails <strong>before</strong> it uploads anything. Then a daily check asks a different question: is the newest archive older than 36 hours? That catches the case where the timer never ran at all, which a failure alert cannot see.</p>
<p>And once a week, a job decrypts the newest archive, unpacks it, and runs an integrity check on the databases inside. If the backup cannot be restored, it is not a backup. It is a file.</p>
<h1 id="silent-failures-are-the-default">Silent failures are the default</h1>
<p>Almost every problem I found that day was silent:</p>
<ul>
<li>The book downloader had the right SABnzbd details but the download <strong>method</strong> was disabled, so every grab was dropped with a warning nobody read.</li>
<li>Downloads completed into a directory the post-processor could not see, because the two containers mounted different paths.</li>
<li>The reading app imported nothing because metadata parsing was off; it fell back to filenames, could not find a series name, and logged &ldquo;unable to parse&rdquo; for every book.</li>
</ul>
<p>None of these produced an error anyone would notice. They produced <strong>nothing</strong>. The system looked fine, and it did nothing.</p>
<p>So my new rule is boring: if a component can fail without anyone noticing, it needs a check that notices. Quiet success is not the same as success.</p>
<h1 id="paths-are-interfaces">Paths are interfaces</h1>
<p>The path mismatch is the one that still annoys me. Two containers, same host storage, different mount points. The download client reported where it put the file, and the post-processor looked somewhere else.</p>
<p>Filesystem paths between containers are an API. They deserve the same care as an HTTP contract: decide the shared tree once, mount it consistently, and write down the assumption. I fixed the mount and now the setup script states the completed path explicitly, so the next rebuild does not rediscover it.</p>
<h1 id="drift-is-a-habit-not-a-tool">Drift is a habit, not a tool</h1>
<p>GitOps is not &ldquo;we use ArgoCD&rdquo;. It is the habit of never hand-editing the thing you claim is declared. I caught myself applying changes imperatively and telling myself I would fix the manifest later. Later is how drift starts.</p>
<p>So I tightened the loop: pull requests only, a hook on the admin workstation that refuses direct pushes to <code>main</code>, and an alert when an app is out of sync or unhealthy for more than fifteen minutes. The cluster repairs itself; the alarm tells me when it cannot.</p>
<p>GitHub&rsquo;s branch protection wants a paid plan for private repos, so the hook is a workaround, not a solution. I wrote that down instead of pretending it was done.</p>
<h1 id="watch-the-watcher">Watch the watcher</h1>
<p>Every monitor in this lab runs inside the lab. If the lab is down, the monitor is down, and the silence looks exactly like health.</p>
<p>The dead-man&rsquo;s switch is the exception: a small timer on the admin workstation outside the cluster, pinging the public status endpoint. Three failures in a row and Telegram gets a message; when it comes back, so does a recovery note.</p>
<p>It is not sophisticated. It is the difference between &ldquo;probably fine&rdquo; and &ldquo;I would know&rdquo;.</p>
<h1 id="small-constraints-catch-you">Small constraints catch you</h1>
<p>The LazyLibrarian API key must be exactly 32 characters. I generated 40, the app accepted it into the config, and the API answered &ldquo;invalid API key&rdquo;. No error about length; just a key that did not work.</p>
<p>That one reminded me that validation is often implicit. A config being written is not the same as it being valid. I now generate the key at the right length, and the setup script stores it where the tools read it.</p>
<h1 id="pin-what-you-do-not-control">Pin what you do not control</h1>
<p>Using <code>latest</code> for someone else&rsquo;s image is a decision to be surprised later. I pinned the images that publish stable versions or digests. The LinuxServer *arr stack still tracks <code>latest</code> and <code>develop</code>; that is a deliberate trade-off and a known gap, not a finished job.</p>
<p>The first attempt at pinning was also a lesson: the release tag was <code>0.33.2</code>, not <code>v0.33.2</code>, and I caught it only because I checked the registry before merging. Verify the thing you are about to depend on.</p>
<h1 id="the-unglamorous-work-is-the-work">The unglamorous work is the work</h1>
<p>None of this made a demo. There is no screenshot. It is a pile of checks, scripts, and boring conventions:</p>
<ul>
<li>one command to rebuild the book stack,</li>
<li>a weekly restore test,</li>
<li>a freshness check,</li>
<li>an external heartbeat,</li>
<li>digests instead of tags,</li>
<li>a written list of the ways it has already broken.</li>
</ul>
<p>That last one might be the most useful artifact. The lab is not impressive because it is complex; it is slowly becoming trustworthy because its failure modes are written down and watched.</p>
<p>If you want the long version (hardware, network, storage, GitOps, services, backups, security, and the full failure table), it lives in the <a href="https://bismigel.dev/projects/bakuran-homelab/">technical reference</a>
.</p>
<h1 id="what-i-would-do-next">What I would do next</h1>
<p>A full disaster-recovery rehearsal, not just a backup verification. Actually rebuild a host, restore the data, and time it. Until I do that, &ldquo;the backups work&rdquo; is still a claim, not a measurement.</p>
]]></content:encoded></item></channel></rss>