<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Homelab on RevoluGame</title><link>http://revolugame.com/categories/homelab/</link><description>Recent content in Homelab on RevoluGame</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 23 Jun 2026 10:00:00 +0100</lastBuildDate><atom:link href="http://revolugame.com/categories/homelab/index.xml" rel="self" type="application/rss+xml"/><item><title>Designing a Homelab Backup Strategy I Can Actually Trust</title><link>http://revolugame.com/p/designing-a-homelab-backup-strategy-i-can-actually-trust/</link><pubDate>Tue, 23 Jun 2026 10:00:00 +0100</pubDate><guid>http://revolugame.com/p/designing-a-homelab-backup-strategy-i-can-actually-trust/</guid><description>&lt;img src="http://revolugame.com/p/designing-a-homelab-backup-strategy-i-can-actually-trust/cover-image.png" alt="Featured image of post Designing a Homelab Backup Strategy I Can Actually Trust" /&gt;&lt;p&gt;Most homelab diagrams start with the fun parts: the NAS, the containers, the dashboards, the automations, the small machines doing useful little jobs around the house. Backup diagrams are usually less glamorous. A few arrows to a NAS, maybe one more arrow to the cloud, and the comforting feeling that important files probably exist in more than one place.&lt;/p&gt;
&lt;p&gt;In short: I want the NAS to be the local backup hub, Synology DSM to handle snapshots and Hyper Backup jobs, USB disks to provide an offline copy, cloud storage to provide encrypted offsite history, and Prometheus plus ntfy to tell me when the system stops doing its job.&lt;/p&gt;
&lt;p&gt;That word, &amp;ldquo;probably&amp;rdquo;, is the problem.&lt;/p&gt;
&lt;p&gt;I do not want a backup strategy that looks reassuring in a diagram. I want one that answers boring, specific questions: what happens if I delete a folder by mistake? What happens if the NAS dies? What happens if ransomware encrypts a mounted share? What happens if storage gets corrupted despite the UPS and clean shutdown path? What happens if I have to rebuild the whole thing on different hardware?&lt;/p&gt;
&lt;p&gt;So this is the target architecture I want my homelab backups to move toward: not just more copies, but copies that fail for different reasons, are encrypted where they leave the house, and are tested often enough that &amp;ldquo;restore&amp;rdquo; is not a theory.&lt;/p&gt;
&lt;h2 id="the-rule-behind-the-design"&gt;The rule behind the design
&lt;/h2&gt;&lt;p&gt;The common version is the 3-2-1 rule: keep three copies, on two different types of media, with one copy offsite. For a homelab, I think the more useful target is closer to 3-2-1-1-0:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;three copies of important data&lt;/li&gt;
&lt;li&gt;two different storage types or systems&lt;/li&gt;
&lt;li&gt;one offsite copy&lt;/li&gt;
&lt;li&gt;one offline or immutable copy&lt;/li&gt;
&lt;li&gt;zero untested restores&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last two matter more than they look. A cloud sync is useful, but it is not the same thing as an offline backup. If a bad script deletes a directory and that deletion syncs perfectly to the cloud, the cloud did its job and I still lost the data. Likewise, a backup I have never restored from is mostly a hope with timestamps.&lt;/p&gt;
&lt;p&gt;The goal is not to back up everything with the same level of paranoia. The goal is to classify data by how painful it would be to lose, then give each class the right recovery path.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Layer&lt;/th&gt;
					&lt;th&gt;Job&lt;/th&gt;
					&lt;th&gt;In this setup&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Local hub&lt;/td&gt;
					&lt;td&gt;Fast recovery and one place to collect backups&lt;/td&gt;
					&lt;td&gt;Synology DSM&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Snapshots&lt;/td&gt;
					&lt;td&gt;Quick rollback from mistakes&lt;/td&gt;
					&lt;td&gt;DSM Snapshot Replication&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Offsite&lt;/td&gt;
					&lt;td&gt;Survive local loss&lt;/td&gt;
					&lt;td&gt;Encrypted Hyper Backup to cloud&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Offline&lt;/td&gt;
					&lt;td&gt;Survive compromised or damaged online copies&lt;/td&gt;
					&lt;td&gt;Rotated USB disks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Monitoring&lt;/td&gt;
					&lt;td&gt;Notice broken backup jobs&lt;/td&gt;
					&lt;td&gt;Prometheus and ntfy&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Restore tests&lt;/td&gt;
					&lt;td&gt;Prove the plan works&lt;/td&gt;
					&lt;td&gt;Scheduled restores from NAS, cloud, and USB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="what-needs-protecting"&gt;What needs protecting
&lt;/h2&gt;&lt;p&gt;In my setup, the important things fall into a few buckets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Irreplaceable data&lt;/strong&gt; is the obvious one: documents, photos, personal notes, scanned paperwork, source repositories, and anything else that cannot be recreated from a package manager or a public download.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service state&lt;/strong&gt; is the data that makes self-hosted apps mine: Docker bind mounts, named volumes, databases, Home Assistant backups, Gitea repositories, application config, and the little bits of state that are easy to forget until a restore fails without them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rebuild information&lt;/strong&gt; is everything needed to reconstruct the machines: compose files, &lt;code&gt;.env&lt;/code&gt; files, systemd units, NUT configuration, firewall notes, package lists, and the &amp;ldquo;why is this weird thing configured this way?&amp;rdquo; documentation that future me will absolutely need.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Convenience data&lt;/strong&gt; is useful but not precious: media files, caches, generated reports, downloads, and anything I would be annoyed to lose but not devastated by.&lt;/p&gt;
&lt;p&gt;Those buckets should not all get the same policy. Photos deserve versioned, offsite, offline protection. A container image cache does not.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Data class&lt;/th&gt;
					&lt;th&gt;Examples&lt;/th&gt;
					&lt;th&gt;Backup policy&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Irreplaceable&lt;/td&gt;
					&lt;td&gt;photos, documents, notes, source repositories&lt;/td&gt;
					&lt;td&gt;NAS, snapshots, encrypted cloud, offline USB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Service state&lt;/td&gt;
					&lt;td&gt;Home Assistant, Gitea, app data, databases&lt;/td&gt;
					&lt;td&gt;app-aware export to NAS, then cloud and USB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Rebuild information&lt;/td&gt;
					&lt;td&gt;compose files, &lt;code&gt;.env&lt;/code&gt; references, NUT config, systemd units&lt;/td&gt;
					&lt;td&gt;Git where safe, NAS backup for secrets and local-only files&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Convenience&lt;/td&gt;
					&lt;td&gt;media, downloads, generated reports&lt;/td&gt;
					&lt;td&gt;NAS if useful, lower retention, no drama&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Ephemeral&lt;/td&gt;
					&lt;td&gt;caches, container images, build artifacts&lt;/td&gt;
					&lt;td&gt;usually not backed up&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-nas-is-the-hub-not-the-backup-strategy"&gt;The NAS is the hub, not the backup strategy
&lt;/h2&gt;&lt;p&gt;The NAS is the center of the design because it is the easiest place for every machine to send backups. In my case that NAS is a Synology running DSM, which gives me a few useful primitives out of the box: shared folders, Time Machine support, snapshots, notifications, USB disk handling, and Hyper Backup for versioned backup jobs. Home Assistant can push scheduled backups to it. Macs can use it as a Time Machine target. Linux machines can send &lt;code&gt;restic&lt;/code&gt;, &lt;code&gt;borg&lt;/code&gt;, &lt;code&gt;kopia&lt;/code&gt;, or plain snapshot artifacts to it. Docker hosts can dump databases and copy application state to it.&lt;/p&gt;
&lt;p&gt;But the NAS is not the strategy by itself. It is just the first aggregation point.&lt;/p&gt;
&lt;p&gt;The layout I want is explicit enough that each source has its own place:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/backups/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; home-assistant/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; macos-time-machine/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ubuntu/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; docker/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; databases/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; native-apps/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sentinel/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; gitea/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; restore-tests/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That structure matters less than the habit behind it: every source should have an obvious owner, an obvious schedule, and an obvious restore procedure. If I cannot tell what created a backup or how to use it, the backup is already weaker than it looks.&lt;/p&gt;
&lt;p&gt;On DSM, I want Snapshot Replication enabled for the important shared folders where it is available. Snapshots are not a substitute for backup, because they live on the same system, but they are excellent for fast recovery from accidental deletion, bad sync jobs, and &amp;ldquo;I changed this file yesterday and now regret it&amp;rdquo; moments.&lt;/p&gt;
&lt;h2 id="docker-apps-need-app-aware-backups"&gt;Docker apps need app-aware backups
&lt;/h2&gt;&lt;p&gt;Backing up Docker compose files is necessary, but not sufficient. A compose file tells me how to start the container; it does not necessarily contain the application state.&lt;/p&gt;
&lt;p&gt;For each Docker app, I want four things backed up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the compose file&lt;/li&gt;
&lt;li&gt;the environment file or secret reference&lt;/li&gt;
&lt;li&gt;bind-mounted application data or named volumes&lt;/li&gt;
&lt;li&gt;database dumps created by the database itself&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is where a lot of homelab backups get fragile. Copying a live database directory may work until the day it does not. For Postgres, MariaDB, SQLite-backed apps, and similar systems, the backup job should either use the application/database&amp;rsquo;s recommended export mechanism or stop/quiesce the service before taking the copy.&lt;/p&gt;
&lt;p&gt;In practice, the pattern should be boring:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;prepare app -&amp;gt; dump database -&amp;gt; snapshot/copy data -&amp;gt; send to NAS -&amp;gt; verify artifact
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The restore procedure should be just as boring:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;create clean app directory -&amp;gt; restore compose/env -&amp;gt; restore data -&amp;gt; import database -&amp;gt; start app
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;If I cannot write that procedure down for an app, I do not really have a backup of that app yet.&lt;/p&gt;
&lt;h2 id="home-assistant-gets-its-own-lane"&gt;Home Assistant gets its own lane
&lt;/h2&gt;&lt;p&gt;Home Assistant OS already has a good backup concept, so I do not want to fight it. The ideal version is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scheduled Home Assistant backups&lt;/li&gt;
&lt;li&gt;automatic copy to the NAS&lt;/li&gt;
&lt;li&gt;NAS backup copied onward to cloud and offline storage&lt;/li&gt;
&lt;li&gt;occasional restore into a test VM or spare install&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last item is the important one. Home Assistant is full of integrations, devices, add-ons, secrets, and local assumptions. A backup file existing on disk is not the same thing as knowing that I can restore it and have the house come back in a sane state.&lt;/p&gt;
&lt;p&gt;For this category, I care less about elegant tooling and more about a tested recovery note: where the backup lives, what credentials I need, what device integrations might need manual attention, and how I know the restore worked.&lt;/p&gt;
&lt;h2 id="the-small-machines-count-too"&gt;The small machines count too
&lt;/h2&gt;&lt;p&gt;It is easy to forget the little infrastructure boxes because they do not feel like data stores. My NUT server is a good example. If it disappeared, I could probably rebuild it from memory, but &amp;ldquo;probably&amp;rdquo; is exactly what this strategy is trying to remove.&lt;/p&gt;
&lt;p&gt;For small utility machines, I want a lightweight backup of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc&lt;/code&gt; files specific to the service&lt;/li&gt;
&lt;li&gt;systemd units and timers&lt;/li&gt;
&lt;li&gt;scripts&lt;/li&gt;
&lt;li&gt;package list or install notes&lt;/li&gt;
&lt;li&gt;any local state that is not disposable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For something like a NUT server, that means backing up &lt;code&gt;/etc/nut/&lt;/code&gt;, notification scripts, and service overrides to the NAS, while also keeping the non-secret parts in Git. The backup does not need to be large. It just needs to make rebuilds boring.&lt;/p&gt;
&lt;h2 id="gitea-is-not-just-on-the-mac"&gt;Gitea is not just &amp;ldquo;on the Mac&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Time Machine is good for recovering a Mac. It is not automatically a good application-level backup for every service that happens to run on that Mac.&lt;/p&gt;
&lt;p&gt;For Gitea, I want a dedicated backup path: repositories, database, &lt;code&gt;app.ini&lt;/code&gt;, custom templates, LFS data if used, and the pieces that make Git-over-SSH work. In my case SSH is enabled through Gitea&amp;rsquo;s built-in SSH server, so the restore procedure needs to account for Gitea&amp;rsquo;s SSH host keys, the configured SSH port, and the user/container mapping that lets Git operations reach the right repositories. Gitea has its own dump command, and that should be part of the plan rather than relying only on a filesystem-level Mac backup.&lt;/p&gt;
&lt;p&gt;The reason is simple: restoring the web UI is not the same thing as restoring the developer workflow. If the repositories and database come back but every remote now fails on &lt;code&gt;git push&lt;/code&gt;, the backup is incomplete.&lt;/p&gt;
&lt;p&gt;The nice property of an app-native Gitea backup is that it can be restored somewhere else. That is the bar I care about. If the Mac dies, I should be able to bring Gitea up on another machine without first resurrecting the Mac exactly as it was.&lt;/p&gt;
&lt;h2 id="cloud-backup-should-be-encrypted-and-versioned"&gt;Cloud backup should be encrypted and versioned
&lt;/h2&gt;&lt;p&gt;The cloud copy should not be a raw mirror of the NAS. It should be an encrypted, versioned backup repository.&lt;/p&gt;
&lt;p&gt;The exact tool matters less than the properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;client-side encryption before data leaves home&lt;/li&gt;
&lt;li&gt;versioned snapshots&lt;/li&gt;
&lt;li&gt;retention policy&lt;/li&gt;
&lt;li&gt;integrity checks&lt;/li&gt;
&lt;li&gt;credentials with the smallest practical permissions&lt;/li&gt;
&lt;li&gt;restore procedure documented outside the backup itself&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;restic&lt;/code&gt;, &lt;code&gt;borg&lt;/code&gt;, &lt;code&gt;kopia&lt;/code&gt;, and similar tools all fit this model better than a blind sync. Since the NAS is Synology DSM, Hyper Backup is also a natural option here: it can send versioned, encrypted backups to cloud providers, rsync destinations, another Synology, or local USB storage. The important part is not the brand of tool, but that the cloud target is a backup repository with history, not just a synchronized copy of today&amp;rsquo;s mistakes.&lt;/p&gt;
&lt;p&gt;The cloud provider is allowed to disappear from the recovery path for local failures, and the local NAS is allowed to disappear from the recovery path for cloud restores. If both are required at the same time, the design has a hidden coupling.&lt;/p&gt;
&lt;h2 id="usb-disks-are-for-offline-recovery"&gt;USB disks are for offline recovery
&lt;/h2&gt;&lt;p&gt;The USB disk is not there because I enjoy plugging in drives. It is there because offline storage survives a different class of failures.&lt;/p&gt;
&lt;p&gt;On DSM, this is a good fit for a Hyper Backup task targeting an external USB disk. An ideal USB backup flow looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;plug in disk -&amp;gt; run backup -&amp;gt; verify -&amp;gt; unmount -&amp;gt; physically disconnect
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Even better, rotate two disks: one at home, one somewhere else. That is less convenient than a permanently attached drive, but convenience is not the job of this copy. Its job is to be unreachable when a compromised machine, broken script, or accidental deletion tries to destroy everything it can see.&lt;/p&gt;
&lt;p&gt;This is the copy I want if the NAS and cloud repository are both logically damaged. Not because that is likely, but because that is the kind of failure that makes every online copy suspect at the same time.&lt;/p&gt;
&lt;h2 id="a-second-nas-is-useful-if-it-changes-the-failure-mode"&gt;A second NAS is useful if it changes the failure mode
&lt;/h2&gt;&lt;p&gt;An offsite NAS would be a good future upgrade, but only if it is not just another always-mounted destination with a different hostname.&lt;/p&gt;
&lt;p&gt;The best version is pull-based: the offsite NAS connects in, pulls encrypted backup artifacts, and stores them with its own retention. That way, if the primary NAS is compromised, it cannot trivially reach out and delete the offsite copy with the same credentials it uses for normal backups.&lt;/p&gt;
&lt;p&gt;If that is too much complexity, cloud plus rotated USB disks may be a better tradeoff. The point is not to collect backup destinations. The point is to avoid shared failure modes.&lt;/p&gt;
&lt;h2 id="monitoring-is-part-of-the-backup-system"&gt;Monitoring is part of the backup system
&lt;/h2&gt;&lt;p&gt;A backup job that fails silently for three months is not a backup job. It is a delayed surprise.&lt;/p&gt;
&lt;p&gt;Every recurring backup should report somewhere when it succeeds and when it fails. In my homelab, that means Prometheus for machine-readable state and history, and ntfy for the human-facing &amp;ldquo;you need to look at this&amp;rdquo; notification. The tooling is less important than the invariant: if a backup stops running, I should find out before I need it.&lt;/p&gt;
&lt;p&gt;DSM&amp;rsquo;s own notifications should be part of this too. If a Hyper Backup task fails, a USB disk is not mounted, a volume degrades, or a snapshot job stops running, that should end up in the same alerting path as the rest of the homelab health checks.&lt;/p&gt;
&lt;p&gt;The signal I want from each job is small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;last successful run&lt;/li&gt;
&lt;li&gt;duration&lt;/li&gt;
&lt;li&gt;size or number of changed files&lt;/li&gt;
&lt;li&gt;destination&lt;/li&gt;
&lt;li&gt;verification status&lt;/li&gt;
&lt;li&gt;retention/prune result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those fields are enough to spot most weirdness: a job that stopped running, a backup that suddenly became tiny, a cloud upload that never finished, or a prune operation that failed and left the repository growing forever.&lt;/p&gt;
&lt;h2 id="restore-tests-make-it-real"&gt;Restore tests make it real
&lt;/h2&gt;&lt;p&gt;This is the part I most want to make non-optional.&lt;/p&gt;
&lt;p&gt;I want a small restore calendar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;monthly: restore a random document or photo from NAS and cloud&lt;/li&gt;
&lt;li&gt;quarterly: restore a Docker app into a temporary directory or VM&lt;/li&gt;
&lt;li&gt;quarterly: restore a Home Assistant backup into a test instance&lt;/li&gt;
&lt;li&gt;yearly: simulate losing the NAS and recover the most important data from cloud or USB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The test does not have to be dramatic. It just has to be real. A restored file should open. A restored database should start. A restored Home Assistant instance should boot far enough to prove the backup is usable.&lt;/p&gt;
&lt;p&gt;The restore notes should live somewhere I can reach during an outage. A recovery plan stored only on the NAS it is meant to recover is a joke with excellent formatting.&lt;/p&gt;
&lt;h2 id="the-target-architecture"&gt;The target architecture
&lt;/h2&gt;&lt;p&gt;The architecture I want to end up with looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Mac
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; Time Machine -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; Gitea app backup -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Ubuntu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; compose/config backup -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; app data + database dumps -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Home Assistant OS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; scheduled backups -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sentinel / NUT server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; config + scripts -&amp;gt; NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; DSM Snapshot Replication for important shares
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; Hyper Backup encrypted cloud backup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; Hyper Backup rotated offline USB backup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; optional offsite NAS pull backup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Monitoring
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;- every backup job reports status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Restore tests
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;- periodically restore from NAS, cloud, and USB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;It is not the most exotic setup. That is a feature. The best backup strategy for a homelab is one I will actually maintain when nothing is on fire.&lt;/p&gt;
&lt;h2 id="my-checklist-before-calling-this-done"&gt;My checklist before calling this done
&lt;/h2&gt;&lt;p&gt;Before I consider this strategy real, I want to be able to check off the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every important service has a documented restore procedure&lt;/li&gt;
&lt;li&gt;every database has an app-aware dump or quiesced backup&lt;/li&gt;
&lt;li&gt;Home Assistant backups are copied beyond the Home Assistant machine&lt;/li&gt;
&lt;li&gt;Gitea has an app-native backup, not just Time Machine coverage&lt;/li&gt;
&lt;li&gt;small utility machines have their service configs backed up&lt;/li&gt;
&lt;li&gt;DSM snapshots are enabled for important shared folders&lt;/li&gt;
&lt;li&gt;Hyper Backup cloud jobs are encrypted, versioned, and periodically verified&lt;/li&gt;
&lt;li&gt;at least one Hyper Backup USB target exists and is disconnected after backup&lt;/li&gt;
&lt;li&gt;backup jobs report success and failure somewhere visible&lt;/li&gt;
&lt;li&gt;restore tests happen on a schedule&lt;/li&gt;
&lt;li&gt;recovery notes are available without depending on the NAS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The real lesson is that &amp;ldquo;where do I copy this?&amp;rdquo; is the wrong first question. The better question is: &amp;ldquo;which failure is this copy supposed to survive?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Once each backup has a job, the architecture becomes easier to reason about. The NAS is for fast local recovery. The cloud is for offsite encrypted history. The USB disk is for offline survival. App-native exports are for portable restores. Monitoring is for noticing when the whole system quietly stops doing its job.&lt;/p&gt;
&lt;p&gt;And restore tests are what turn the drawing from a comforting picture into a system I can actually trust.&lt;/p&gt;</description></item><item><title>My homelab stack in 2026: what runs, why, and how it all connects</title><link>http://revolugame.com/p/my-homelab-stack-in-2026-what-runs-why-and-how-it-all-connects/</link><pubDate>Sat, 20 Jun 2026 10:00:00 +0100</pubDate><guid>http://revolugame.com/p/my-homelab-stack-in-2026-what-runs-why-and-how-it-all-connects/</guid><description>&lt;img src="http://revolugame.com/p/my-homelab-stack-in-2026-what-runs-why-and-how-it-all-connects/pexels-cookiecutter-37605910.jpg" alt="Featured image of post My homelab stack in 2026: what runs, why, and how it all connects" /&gt;&lt;p&gt;I&amp;rsquo;m not going to make the case for self-hosting here. If you&amp;rsquo;re reading this, you already get it. What I want to do instead is be honest about what I actually run, why I made the specific choices I made, and - more interestingly - how the pieces talk to each other in ways that weren&amp;rsquo;t always planned from the start.&lt;/p&gt;
&lt;p&gt;In short: my 2026 homelab runs on a mix of Raspberry Pis, a Mac Mini, and an Ubuntu mini PC. Traefik handles routing, Tailscale provides remote access, Prometheus watches the stack, Ntfy connects notifications, and local AI runs through Ollama and OpenWebUI.&lt;/p&gt;
&lt;p&gt;The stack runs across four machines. The bulk of Docker workloads are split between a Mac Mini and an Ubuntu mini PC. Network infrastructure - Traefik and CoreDNS - runs on a Raspberry Pi 4, which also handles the NUT server for UPS management; keeping the network layer on its own always-on hardware means a container crash elsewhere doesn&amp;rsquo;t take down routing or DNS. Home Assistant runs on a Raspberry Pi 5 with the Hailo 8 AI HAT. &lt;a class="link" href="https://frigate.video/" target="_blank" rel="noopener"
 &gt;Frigate&lt;/a&gt; runs alongside it as a Home Assistant add-on, which is what gives it direct access to the Hailo 8 for hardware-accelerated camera object detection - no GPU needed in the main machines for that workload.&lt;/p&gt;
&lt;p&gt;The mental model that makes sense of the whole thing: everything gets served through Traefik, everything ships metrics to Prometheus, and Ntfy acts as the notification bus that ties async events together. Most of the rest are applications that plug into those three rails.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Category&lt;/th&gt;
					&lt;th&gt;Tools&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Network&lt;/td&gt;
					&lt;td&gt;Tailscale, Traefik, CoreDNS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Dev &amp;amp; CI/CD&lt;/td&gt;
					&lt;td&gt;Gitea, GitHub, Woodpecker CI, Docker Registry, WUD&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AI&lt;/td&gt;
					&lt;td&gt;Ollama, OpenWebUI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Search&lt;/td&gt;
					&lt;td&gt;SearXNG&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Documents&lt;/td&gt;
					&lt;td&gt;Paperless-NGX, Paperless-AI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Passwords&lt;/td&gt;
					&lt;td&gt;Vaultwarden&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Monitoring &amp;amp; management&lt;/td&gt;
					&lt;td&gt;Prometheus, Portainer, Homer&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Notifications &amp;amp; sharing&lt;/td&gt;
					&lt;td&gt;Ntfy, Pairdrop&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Home automation&lt;/td&gt;
					&lt;td&gt;Home Assistant, Frigate&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 subgraph pi4["Pi 4 — Network layer"]
 TS[Tailscale]
 TR[Traefik]
 DNS[CoreDNS]
 end

 subgraph main["Mac Mini + Ubuntu — Services"]
 GIT[Gitea]
 WP[Woodpecker CI]
 REG[Docker Registry]
 WUD[WUD]
 OLLAMA[Ollama]
 OWU[OpenWebUI]
 SEARX[SearXNG]
 PAI[Paperless-AI]
 PAPER[Paperless-NGX]
 NTFY[Ntfy]
 end

 subgraph pi5["Pi 5 — Home automation"]
 HA[Home Assistant]
 FRIGATE[Frigate]
 end

 TS --&gt; TR
 GIT --&gt; WP --&gt; REG
 WUD --&gt;|alert| NTFY
 OLLAMA --&gt; OWU
 OLLAMA --&gt; PAI --&gt; PAPER
 OWU --&gt;|search| SEARX
 FRIGATE --&gt; HA
 HA --&gt;|notify| NTFY&lt;/pre&gt;&lt;h2 id="the-foundation"&gt;The foundation
&lt;/h2&gt;&lt;h3 id="traefik"&gt;Traefik
&lt;/h3&gt;&lt;p&gt;Everything HTTP goes through &lt;a class="link" href="https://traefik.io/traefik/" target="_blank" rel="noopener"
 &gt;Traefik&lt;/a&gt;. It&amp;rsquo;s the reverse proxy in front of every Docker-hosted service, and the main reason I chose it over Nginx or Caddy is Docker-native autodiscovery. When I bring up a new container with the right labels, it appears behind a subdomain with automatic TLS, no config file reload required. That removes enough friction that I&amp;rsquo;m less tempted to leave things running unproxied on bare ports.&lt;/p&gt;
&lt;p&gt;Traefik handles Let&amp;rsquo;s Encrypt certificate issuance and renewal. Services that aren&amp;rsquo;t on the public internet use a DNS-01 challenge, so they get valid certs without being exposed to the web. The rest of the stack is effectively Traefik labels plus container configs all the way down.&lt;/p&gt;
&lt;h3 id="tailscale"&gt;Tailscale
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://tailscale.com/" target="_blank" rel="noopener"
 &gt;Tailscale&lt;/a&gt; is how every machine in the stack is reachable from outside the local network. All four machines are on the same Tailnet, which means I can reach any service from anywhere without opening ports or maintaining a VPN server.&lt;/p&gt;
&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;Note&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;Tailscale isn&amp;rsquo;t fully self-hosted — the coordination server is Tailscale&amp;rsquo;s. The traffic itself is peer-to-peer and never leaves the devices, but the key exchange goes through their infrastructure. For a homelab, that trade-off is easy to accept; for stricter control, &lt;a class="link" href="https://headscale.net/" target="_blank" rel="noopener"
 &gt;Headscale&lt;/a&gt; is the self-hosted alternative.&lt;/p&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h3 id="coredns"&gt;CoreDNS
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://coredns.io/" target="_blank" rel="noopener"
 &gt;CoreDNS&lt;/a&gt; handles local name resolution. All internal subdomains resolve to the local machine without ever leaving the network, which means Traefik&amp;rsquo;s label-based routing is actually usable on every device without editing hosts files or relying on split-horizon DNS from the router. CoreDNS sits upstream of the system resolver and forwards anything it doesn&amp;rsquo;t own to the public DNS of choice. It&amp;rsquo;s invisible when it works, which is most of the time.&lt;/p&gt;
&lt;h2 id="dev--cicd-the-pipeline"&gt;Dev &amp;amp; CI/CD: the pipeline
&lt;/h2&gt;&lt;p&gt;This is where the most deliberate architecture lives, because I wanted something that felt like a real deployment pipeline rather than manually building and copying images around.&lt;/p&gt;
&lt;h3 id="gitea"&gt;Gitea
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://about.gitea.com/" target="_blank" rel="noopener"
 &gt;Gitea&lt;/a&gt; is where all private repositories live: infra configs, personal projects, anything I don&amp;rsquo;t want on a third-party server. Public projects still go to &lt;a class="link" href="https://github.com" target="_blank" rel="noopener"
 &gt;GitHub&lt;/a&gt; because that&amp;rsquo;s where the audience is, but a number of those GitHub repositories are mirrored back to Gitea as a local backup. The split is simple: Gitea for control and resilience, GitHub for reach.&lt;/p&gt;
&lt;h3 id="woodpecker-ci"&gt;Woodpecker CI
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://woodpecker-ci.org/" target="_blank" rel="noopener"
 &gt;Woodpecker CI&lt;/a&gt; is the pipeline runner, hooked directly into Gitea webhooks. Push to a branch, a pipeline runs. The config lives as a &lt;code&gt;.woodpecker.yml&lt;/code&gt; at the repo root, which means pipeline definitions are versioned alongside the code. Woodpecker builds Docker images and pushes them to a local registry on the same host.&lt;/p&gt;
&lt;h3 id="local-docker-registry"&gt;Local Docker registry
&lt;/h3&gt;&lt;p&gt;A plain Docker registry container, served behind Traefik. Woodpecker pushes here; &lt;code&gt;docker compose&lt;/code&gt; pulls from here. Keeping images local means builds are fast, no rate limits, and nothing depends on an external registry being up. Not sophisticated. Exactly as much complexity as needed.&lt;/p&gt;
&lt;h3 id="wud---whats-up-docker"&gt;WUD - What&amp;rsquo;s Up Docker
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://getwud.github.io/wud/" target="_blank" rel="noopener"
 &gt;WUD&lt;/a&gt; watches running containers and detects when upstream image versions are newer than what&amp;rsquo;s deployed. It doesn&amp;rsquo;t auto-update by itself in my setup, I use it as a detection layer. When it spots a new version, it fires a notification through Ntfy, which I&amp;rsquo;ll get to shortly. The result is that upstream updates surface as notifications I can act on rather than surprises I discover when something breaks.&lt;/p&gt;
&lt;h2 id="local-ai-ollama--openwebui"&gt;Local AI: Ollama + OpenWebUI
&lt;/h2&gt;&lt;p&gt;I run &lt;a class="link" href="https://ollama.com/" target="_blank" rel="noopener"
 &gt;Ollama&lt;/a&gt; for local model inference. The main draws are the obvious ones: no data leaves the machine, no per-token cost, models available offline. Local models cover summarization, document classification, and general Q&amp;amp;A; tasks where a smaller model is good enough and keeping data local matters.&lt;/p&gt;
&lt;p&gt;Code assistance is the clear exception: small models aren&amp;rsquo;t reliable enough there, so that goes to cloud APIs - Claude or Codex depending on the task. &lt;a class="link" href="https://openwebui.com/" target="_blank" rel="noopener"
 &gt;OpenWebUI&lt;/a&gt; makes the split seamless: it sits in front of Ollama but also accepts API keys for cloud providers. In practice I open one interface and pick a model from a dropdown rather than switching tools. Local by default, cloud when it&amp;rsquo;s actually worth it.&lt;/p&gt;
&lt;p&gt;OpenWebUI also connects to SearXNG as a search tool, which means it can pull current information without phoning home to a commercial search provider.&lt;/p&gt;
&lt;h2 id="search-searxng"&gt;Search: SearXNG
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://docs.searxng.org/" target="_blank" rel="noopener"
 &gt;SearXNG&lt;/a&gt; is my default search engine on all devices. It&amp;rsquo;s a meta-search engine. It queries multiple sources and aggregates results. But the key property is that queries don&amp;rsquo;t get tied to an account or used to build a profile. Results are good enough for 95% of searches, and for the other 5% I have a single click to fall back to whatever source I want.&lt;/p&gt;
&lt;p&gt;The setup is minimal: one container behind Traefik, set as the default search engine in the browser. It&amp;rsquo;s one of those things that took twenty minutes to deploy and I&amp;rsquo;ve never thought about since.&lt;/p&gt;
&lt;h2 id="documents-paperless-ngx--paperless-ai"&gt;Documents: Paperless-NGX + Paperless-AI
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://docs.paperless-ngx.com/" target="_blank" rel="noopener"
 &gt;Paperless-NGX&lt;/a&gt; handles all document management. Scan or drop a PDF into the inbox, it gets OCR&amp;rsquo;d, indexed, and stored. The tagging and correspondent system means I can find anything in seconds rather than digging through a folder hierarchy.&lt;/p&gt;
&lt;p&gt;On top of that I run &lt;a class="link" href="https://github.com/clusterzx/paperless-ai" target="_blank" rel="noopener"
 &gt;Paperless-AI&lt;/a&gt;, which hooks into the Paperless-NGX API and uses a local Ollama model to automatically suggest tags, correspondents, titles (and various other custom properties) as documents come in. This closes a loop with the AI section: Ollama isn&amp;rsquo;t just a chat model, it&amp;rsquo;s doing practical classification work for real files. The whole thing runs locally, so no document content touches an external service.&lt;/p&gt;
&lt;h2 id="password-management-vaultwarden"&gt;Password management: Vaultwarden
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/dani-garcia/vaultwarden" target="_blank" rel="noopener"
 &gt;Vaultwarden&lt;/a&gt; is a self-hosted Bitwarden-compatible server. All credentials live locally, sync across devices through the standard Bitwarden clients, and never touch a third-party server. It&amp;rsquo;s one of those services where the self-hosted case is unusually strong: the upstream Bitwarden clients are excellent, Vaultwarden is a drop-in replacement, and keeping your password vault on your own hardware removes a meaningful point of trust.&lt;/p&gt;
&lt;blockquote class="alert alert-tip"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;💡&lt;/span&gt;
 &lt;span class="alert-title"&gt;Tip&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;A password vault is the one thing you cannot lose and cannot recover from a partial backup. I wrote a &lt;a class="link" href="http://revolugame.com/p/backing-up-the-one-credential-that-cant-be-wrong/" &gt;dedicated post about the backup architecture&lt;/a&gt; covering how I handle this specifically for Vaultwarden.&lt;/p&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h2 id="monitoring--management"&gt;Monitoring &amp;amp; management
&lt;/h2&gt;&lt;h3 id="prometheus"&gt;Prometheus
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://prometheus.io/" target="_blank" rel="noopener"
 &gt;Prometheus&lt;/a&gt; scrapes metrics from the stack. Node exporter covers the host, cAdvisor covers containers, and individual services expose their own endpoints where supported. The main value isn&amp;rsquo;t dashboards (though those exist) - it&amp;rsquo;s having a queryable record of system state over time, and a place to hook alerts when something drifts.&lt;/p&gt;
&lt;h3 id="portainer"&gt;Portainer
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://www.portainer.io/" target="_blank" rel="noopener"
 &gt;Portainer&lt;/a&gt; is the visual management layer. I use it for quick container inspection, pulling logs, and managing stacks without SSHing in every time. It doesn&amp;rsquo;t replace Prometheus, they have different jobs. Prometheus tells me what happened and when; Portainer tells me what&amp;rsquo;s running right now and lets me poke at it.&lt;/p&gt;
&lt;h3 id="homer"&gt;Homer
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/bastienwirtz/homer" target="_blank" rel="noopener"
 &gt;Homer&lt;/a&gt; is a static dashboard - a single page with links to every service, configured via a YAML file. It&amp;rsquo;s the least technical piece in the stack and the one my family actually uses. Rather than memorizing subdomains or digging through bookmarks, everyone has Homer as a home page: a clean grid of icons that opens whatever they need. The split between Portainer and Homer is intentional - Portainer is for me, Homer is for everyone else.&lt;/p&gt;
&lt;h2 id="communication--file-sharing"&gt;Communication &amp;amp; file sharing
&lt;/h2&gt;&lt;h3 id="ntfy"&gt;Ntfy
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://ntfy.sh/" target="_blank" rel="noopener"
 &gt;Ntfy&lt;/a&gt; is the thread that ties the whole async event model together. It&amp;rsquo;s a self-hosted push notification server. HTTP &lt;code&gt;POST&lt;/code&gt; to a topic, and every subscribed client gets a notification. Woodpecker sends build results here. WUD sends image update alerts here. Home Assistant sends automation notifications here. Having one place where things send notifications means I can manage subscriptions in one app and stop checking dashboards compulsively.&lt;/p&gt;
&lt;p&gt;The pattern is simple enough that anything can use it: if a script or service needs to tell me something happened, it makes an HTTP request. No SDK, no auth complexity, just a POST.&lt;/p&gt;
&lt;h3 id="pairdrop"&gt;Pairdrop
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://pairdrop.net/" target="_blank" rel="noopener"
 &gt;Pairdrop&lt;/a&gt; is AirDrop for the local network, any device on the LAN can discover others and transfer files peer-to-peer through the browser. No account, no cloud relay, no app install. I use it constantly for moving files between phone, laptop, and desktop without thinking about it.&lt;/p&gt;
&lt;h2 id="home-automation-home-assistant"&gt;Home automation: Home Assistant
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://www.home-assistant.io/" target="_blank" rel="noopener"
 &gt;Home Assistant&lt;/a&gt; is the one thing in the stack that runs on bare OS rather than Docker. It&amp;rsquo;s Home Assistant OS on a dedicated Raspberry Pi 5, which is a deliberate choice: the add-on ecosystem, hardware device support, and the supervisor layer all work better outside a container. Trying to run it in Docker introduced enough friction with USB devices and networking that the clean answer was to give it its own machine.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://frigate.video/" target="_blank" rel="noopener"
 &gt;Frigate&lt;/a&gt; runs as a Home Assistant add-on rather than a standalone container, and that placement matters: it gives Frigate direct access to the Hailo 8 AI HAT on the Pi 5 for hardware-accelerated object detection on camera streams. Running it as an add-on keeps the integration tight and avoids the networking gymnastics that come with trying to expose a hardware accelerator across container boundaries.&lt;/p&gt;
&lt;p&gt;It integrates back into the rest of the stack through Ntfy, automations that need to notify me fire an HTTP call to the same notification server everything else uses. One less thing to configure separately.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next
&lt;/h2&gt;&lt;p&gt;One thing I&amp;rsquo;m actively removing is n8n. On paper it&amp;rsquo;s a great fit — nice UI, trivial to set up, an enormous library of nodes. In practice, for the automations I actually run, it&amp;rsquo;s massively oversized. A tool that big has a way of becoming its own maintenance surface, and when I look at what I&amp;rsquo;m using it for, most of it is simple enough to replace with a script and an HTTP call to Ntfy. Sometimes the right answer is less, not more.&lt;/p&gt;
&lt;p&gt;The stack has been stable enough that I&amp;rsquo;ve been iterating on individual services rather than adding new ones. The pieces that took the most time to get right were the CI/CD pipeline (getting Woodpecker, the local registry, and WUD to work as a coherent unit) and Paperless-AI (tuning the prompts so document classification is actually useful rather than just technically running).&lt;/p&gt;
&lt;p&gt;If any of this is useful as a starting point, most of these services have reasonable official documentation and active communities. The architecture isn&amp;rsquo;t novel, it&amp;rsquo;s mostly standard self-hosting patterns assembled with some thought about how the parts should talk to each other.&lt;/p&gt;</description></item><item><title>Designing Single-Purpose Agents Instead of One Big Automation Script</title><link>http://revolugame.com/p/designing-single-purpose-agents-instead-of-one-big-automation-script/</link><pubDate>Wed, 17 Jun 2026 10:00:00 +0100</pubDate><guid>http://revolugame.com/p/designing-single-purpose-agents-instead-of-one-big-automation-script/</guid><description>&lt;img src="http://revolugame.com/p/designing-single-purpose-agents-instead-of-one-big-automation-script/pexels-introspectivedsgn-22043568.jpg" alt="Featured image of post Designing Single-Purpose Agents Instead of One Big Automation Script" /&gt;&lt;p&gt;&amp;ldquo;Agent&amp;rdquo; has become one of those words that means everything and nothing this year. Before it was a hype term, I&amp;rsquo;d already ended up with a small flock of them in my homelab. Not because I was chasing a trend, but because I kept hitting the same wall every time I tried to write One Big Script: it grew a dozen unrelated responsibilities, and a bug in one of them risked taking down all of them.&lt;/p&gt;
&lt;p&gt;In short: I prefer many small, single-purpose automation agents over one large script because each agent has a narrow job, a clear output contract, and an independent schedule. The system stays maintainable because agents communicate through JSON artifacts, one notification channel, and one dashboard.&lt;/p&gt;
&lt;p&gt;So instead, every recurring chore in my homelab is its own small, independently-scheduled program. There turned out to be more of them than I expected once I actually sat down and counted.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;A note for muggles:&lt;/strong&gt; the repo behind all this is named &lt;code&gt;hogwarts&lt;/code&gt;, and every agent gets sorted to match. Once you start naming services after wizards, it turns out you owe each one an in-character job description, whether it asked for one or not.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The standing watch.&lt;/strong&gt; Four observers poll continuously and report into one correlator every five minutes. This is the layer that exists so I find out about a problem before it becomes a 3am page instead of after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Argus Filch&lt;/strong&gt; watches running Docker containers for restart loops, failed healthchecks, and containers that just quietly vanish.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astronomy Tower&lt;/strong&gt; polls Prometheus for firing alerts, down scrape targets, and recording rules that stopped working without telling anyone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marauder&amp;rsquo;s Map&lt;/strong&gt; scans the UniFi network for offline devices, WAN failover events, and firewall rules that drifted open.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mad-Eye&amp;rsquo;s Watch&lt;/strong&gt; tracks TLS certificate expiry across configured endpoints. Constant vigilance: a warning at 30 days, a critical at 7.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Headmaster&lt;/strong&gt; is the one role on this list that isn&amp;rsquo;t single-purpose by design. Its entire job is reading what the other four decided was worth reporting and correlating that into one status, surfaced as an incident only when it&amp;rsquo;s actually worth one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The daily and weekly chores.&lt;/strong&gt; These run on their own cron schedules and never talk to each other directly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Molly&amp;rsquo;s Cupboard&lt;/strong&gt; reviews the Home Assistant entity list weekly: unavailable entities, missing or duplicate names, disabled automations. (Molly Weasley: keeps the household running, judges your clutter lovingly.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rita&amp;rsquo;s Desk&lt;/strong&gt; is the RSS morning digest: feeds in, previous day&amp;rsquo;s articles out, ranked against persistent tag scores I vote on. Deterministic by design, no LLM in the loop. (Never met a headline she wouldn&amp;rsquo;t print, but at least she always sources it.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kreacher&amp;rsquo;s Kitchen&lt;/strong&gt; plans the week&amp;rsquo;s meals from my recipe library and a couple of trusted cooking sites. (Grumbles the entire time, still gets dinner on the table.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Library&lt;/strong&gt; picks a tech topic every night, gathers sources, and writes a 5-minute digest plus a 15-20 minute deep dive. (Lives in the package manifest as &lt;code&gt;research-digest&lt;/code&gt;, but it spends every night in the Restricted Section, so the Library it is.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Madam Pince&amp;rsquo;s Catalogue&lt;/strong&gt; lists every running container and cross-checks it against the service directories in the infra repo, flagging any container that has no matching documentation. (A very particular librarian: every book gets catalogued, or it gets confiscated.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dobby&amp;rsquo;s Rounds&lt;/strong&gt; is the homelab&amp;rsquo;s free elf: weekly housekeeping that prunes old snapshots, reports, and state files before they pile up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O.W.L.s&lt;/strong&gt; is the daily infrastructure audit: config drift, open ports, compliance. Read-only and deliberately paranoid about it. (Ordinary Wizarding Level exams: thorough, exhausting, and not interested in your excuses.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auror Office&lt;/strong&gt; is the daily cross-domain security digest, correlating O.W.L.s&amp;rsquo; findings with auth logs, Docker posture, and the network observers above into one report. (No badge, but it does go looking for dark wizards. I&amp;rsquo;ve written about &lt;a class="link" href="http://revolugame.com/p/homelab-personal-soc/" &gt;how this one and O.W.L.s work together&lt;/a&gt; in more detail elsewhere.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;hellip;and others&lt;/strong&gt;, including media management, recommendations, and a handful more in the same spirit. Small enough that listing every one of them would be its own blog post.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thirteen-plus names, just as many jobs. Outside of the two correlators built specifically to know about everyone else — the Headmaster and the Auror Office — not one of them needs to care that the rest exist.&lt;/p&gt;
&lt;h2 id="the-three-conventions-that-make-this-work"&gt;The three conventions that make this work
&lt;/h2&gt;&lt;p&gt;None of these agents are individually clever. What makes the &lt;em&gt;flock&lt;/em&gt; manageable is that they all obey the same three small contracts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. One artifact format.&lt;/strong&gt; Every agent writes its result as JSON (and often a companion Markdown note) to its own &lt;code&gt;outbox/latest/&lt;/code&gt; path. A &amp;ldquo;latest&amp;rdquo; pointer plus a timestamped archive, every time. No agent reads another agent&amp;rsquo;s outbox directly. If something needs cross-referencing, that&amp;rsquo;s a different, explicitly-correlating agent&amp;rsquo;s job, not an implicit dependency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. One notification channel.&lt;/strong&gt; Every agent that needs to tell me something pushes through the same ntfy topic convention, with a deep link back into wherever the full detail lives. I don&amp;rsquo;t maintain five different alerting integrations; I maintain one, and every agent is a thin client of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. One aggregation point.&lt;/strong&gt; A single dashboard reads everyone&amp;rsquo;s &lt;code&gt;outbox/latest/&lt;/code&gt; and renders it. It doesn&amp;rsquo;t collect anything itself. It has no Docker access, no Home Assistant credentials, no API keys. It&amp;rsquo;s a pure read layer over JSON files other things produced. That&amp;rsquo;s the only place in the whole system that&amp;rsquo;s allowed to know all the agents exist.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the entire integration surface. Three conventions, and I can add a sixth agent tomorrow without touching the other five.&lt;/p&gt;
&lt;h2 id="why-decompose-instead-of-consolidate"&gt;Why decompose instead of consolidate
&lt;/h2&gt;&lt;p&gt;The obvious objection: isn&amp;rsquo;t five small things more to maintain than one big thing? In my experience, no. For the same reason a set of small services usually beats a monolith at work.&lt;/p&gt;
&lt;p&gt;A bug in Peeves&amp;rsquo; Trakt pagination cannot break Molly&amp;rsquo;s Home Assistant checks, because they don&amp;rsquo;t share a process, a deploy, or a schedule. I can test each one in complete isolation with a fixture file instead of live credentials. I can hand any single agent&amp;rsquo;s directory to a contributor - human or an AI coding agent - and they have everything they need to understand and change it, without first having to load the other four into their head. And when I retire one (Peeves only matters because I still have a media server; that won&amp;rsquo;t be true forever), deleting it is deleting a directory, not untangling a shared module.&lt;/p&gt;
&lt;p&gt;This is the same lesson as service boundaries and team topologies at any reasonably-sized engineering org: the interface between components should be small, explicit, and boring, and almost all of the design effort should go into keeping it that way. Not into making any individual component clever. The cleverness, if there is any, belongs inside one agent&amp;rsquo;s narrow walls, where it can&amp;rsquo;t leak.&lt;/p&gt;
&lt;h2 id="the-boring-plumbing-is-the-point"&gt;The boring plumbing is the point
&lt;/h2&gt;&lt;p&gt;None of the five agents above is doing anything technically hard. RSS parsing, a REST API client, a cron job - this is all stuff any of us could write in an afternoon. The actual design work was deciding, up front, that &amp;ldquo;outbox JSON + one notification channel + one dashboard&amp;rdquo; would be the &lt;em&gt;entire&lt;/em&gt; contract between them, and then refusing to let any agent reach around it.&lt;/p&gt;
&lt;p&gt;That discipline is cheap when you only have one agent. It&amp;rsquo;s the only thing that keeps five (or fifteen) from turning back into the One Big Script I was trying to avoid in the first place.&lt;/p&gt;</description></item><item><title>Backing Up the One Credential That Can't Be Wrong</title><link>http://revolugame.com/p/backing-up-the-one-credential-that-cant-be-wrong/</link><pubDate>Mon, 15 Jun 2026 10:00:00 +0100</pubDate><guid>http://revolugame.com/p/backing-up-the-one-credential-that-cant-be-wrong/</guid><description>&lt;img src="http://revolugame.com/p/backing-up-the-one-credential-that-cant-be-wrong/pexels-padrinan-2882630.jpg" alt="Featured image of post Backing Up the One Credential That Can't Be Wrong" /&gt;&lt;p&gt;Most things in my homelab can fail and I shrug. A container restarts, a dashboard is stale for an hour, a media file gets deleted by mistake: annoying, recoverable, fine. The password vault is not in that category. If it&amp;rsquo;s wrong, or gone, or merely unreachable at the wrong moment, I lose access to everything else at once. It&amp;rsquo;s the one piece of infrastructure that earns the extra paranoia.&lt;/p&gt;
&lt;p&gt;In short: my password vault backup strategy keeps three copies that survive different failure modes: the primary vault, a live self-hosted Vaultwarden mirror, and an offline KeePass archive. The important part is not the number of backups, but making sure they do not all fail for the same reason.&lt;/p&gt;
&lt;p&gt;So instead of &amp;ldquo;back it up somewhere,&amp;rdquo; I sat down and asked the question I&amp;rsquo;d ask at work for any single point of failure: &lt;em&gt;which specific failure does each copy need to survive?&lt;/em&gt; That question is what actually shaped the design. Not &amp;ldquo;more backups.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="three-copies-three-different-failure-modes"&gt;Three copies, three different failure modes
&lt;/h2&gt;&lt;p&gt;The vault lives day-to-day in Dashlane. Around it, a script keeps two more copies current:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A dated, offline KeePass &lt;code&gt;.kdbx&lt;/code&gt; archive.&lt;/strong&gt; The script exports the vault, converts it to KeePass format, and syncs the file to NAS over rsync. This file needs nothing else to be true. No Dashlane account, no Vaultwarden instance, no network to be opened. KeePassXC plus a master passphrase is the entire recovery path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A live Vaultwarden mirror.&lt;/strong&gt; The script wipes and re-imports a self-hosted Vaultwarden instance on every run, so it never drifts and never accumulates stale duplicates. Unlike the &lt;code&gt;.kdbx&lt;/code&gt;, this one behaves like a normal vault app day-to-day. Useful if Dashlane itself is the thing that&amp;rsquo;s unavailable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The original, in Dashlane.&lt;/strong&gt; Still the primary, still the one I actually use day to day.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these answers a different question. If Dashlane has an outage or I get locked out of the account, Vaultwarden keeps working normally. If my entire home network and every service on it is down, or I&amp;rsquo;m on a borrowed machine with nothing installed, the &lt;code&gt;.kdbx&lt;/code&gt; file plus a passphrase I&amp;rsquo;ve memorized is the whole recovery procedure. No SSH keys, no app, no account, no network. If the NAS itself dies, the live Vaultwarden mirror (running elsewhere) and Dashlane are both still fine.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the test I&amp;rsquo;d apply to any redundancy claim: two backups that die from the same root cause aren&amp;rsquo;t two backups, they&amp;rsquo;re one backup with extra steps. These three don&amp;rsquo;t share a single point of failure with each other.&lt;/p&gt;
&lt;h2 id="the-part-that-matters-more-than-the-architecture"&gt;The part that matters more than the architecture
&lt;/h2&gt;&lt;p&gt;Diagrams of &amp;ldquo;three copies in three places&amp;rdquo; are easy to draw and easy to get wrong in the implementation details, and the details are where a vault backup script can quietly turn into a liability instead of a safety net.&lt;/p&gt;
&lt;p&gt;The script exports the raw vault to CSV in plaintext before converting it. There&amp;rsquo;s no way around that, the conversion tool needs plaintext input. So the entire design constraint became: &lt;em&gt;that plaintext must never outlive the script&amp;rsquo;s own execution.&lt;/em&gt; It&amp;rsquo;s written to a private temp directory, and a shell &lt;code&gt;trap&lt;/code&gt; deletes it on every exit path. Success, failure, or interruption, not just the happy path. The master passphrase that protects the &lt;code&gt;.kdbx&lt;/code&gt; is treated as its own tier-zero secret: it lives outside the repo, outside the backup, memorized or written down somewhere physically safe, because it&amp;rsquo;s the one credential that unlocks the credential store.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a small thing, a &lt;code&gt;trap ... EXIT&lt;/code&gt; instead of cleanup code only at the bottom of the script. But it&amp;rsquo;s exactly the kind of detail that&amp;rsquo;s invisible when it works and catastrophic when it doesn&amp;rsquo;t. I&amp;rsquo;d rather the script crash and still clean up after itself than ship a feature and leave a plaintext export sitting in a temp folder because someone hit Ctrl-C at the wrong moment.&lt;/p&gt;
&lt;h2 id="threat-model-your-one-credential-that-cant-be-wrong"&gt;Threat-model your one credential that can&amp;rsquo;t be wrong
&lt;/h2&gt;&lt;p&gt;Every system has at least one of these. The credential, the key, the record that everything else assumes is correct and available. For most of us it&amp;rsquo;s a password vault; for some it might be a signing key or a recovery seed.&lt;/p&gt;
&lt;p&gt;The exercise worth doing isn&amp;rsquo;t &amp;ldquo;add more backups.&amp;rdquo; It&amp;rsquo;s listing the ways that one thing could become unavailable or wrong, and checking that your copies don&amp;rsquo;t all fail for the same reason. If they do, you&amp;rsquo;ve built redundancy theater, not redundancy.&lt;/p&gt;</description></item><item><title>Running a Personal SOC: Bringing Production Security Practices Home</title><link>http://revolugame.com/p/running-a-personal-soc-bringing-production-security-practices-home/</link><pubDate>Fri, 12 Jun 2026 10:00:00 +0100</pubDate><guid>http://revolugame.com/p/running-a-personal-soc-bringing-production-security-practices-home/</guid><description>&lt;img src="http://revolugame.com/p/running-a-personal-soc-bringing-production-security-practices-home/pexels-dan-nelson-1667453-4973899.jpg" alt="Featured image of post Running a Personal SOC: Bringing Production Security Practices Home" /&gt;&lt;p&gt;At work, nobody questions why we have logging, alerting, and a daily look at what changed overnight. At home, the same network runs a NAS, a media stack, Home Assistant, and a handful of containers. And for years my only &amp;ldquo;security monitoring&amp;rdquo; was noticing something was broken.&lt;/p&gt;
&lt;p&gt;So I built myself a small, read-only security operations setup for the homelab: a daily audit script and a cross-domain digest agent that correlates it with everything else running on the network. Nothing here is novel security research. The interesting part is which production habits turned out to be worth carrying home, and which ones I deliberately left at the office.&lt;/p&gt;
&lt;h2 id="two-layers-not-one"&gt;Two layers, not one
&lt;/h2&gt;&lt;p&gt;The setup is split into two pieces with different jobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The daily audit&lt;/strong&gt; is the boring, deterministic layer. Once a day it collects, locally and read-only:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;listening sockets, flagging anything bound to a wildcard interface&lt;/li&gt;
&lt;li&gt;Docker/container posture: privileged containers, dangerous bind mounts, host network mode, dangerous capabilities&lt;/li&gt;
&lt;li&gt;systemd service drift against an expected allowlist&lt;/li&gt;
&lt;li&gt;a local secrets/config hygiene scan (path, line, and pattern only - never the matched value)&lt;/li&gt;
&lt;li&gt;cached &lt;code&gt;apt list --upgradable&lt;/code&gt;, optionally enriched with a &lt;code&gt;trivy fs&lt;/code&gt; scan&lt;/li&gt;
&lt;li&gt;whether the monitoring artifacts and timers it depends on are actually present&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It writes a deterministic Markdown digest and a JSON report to a local outbox. That&amp;rsquo;s it. No remediation, no service restarts, no firewall changes. The rule I gave it was &amp;ldquo;assume breach, trust no single signal, change nothing during observation.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The SOC agent&lt;/strong&gt; is the correlation layer on top. It runs once a day and pulls together SSH auth logs (brute-force detection, unexpected successful logins, elevated sudo activity), Docker security posture, UniFi network signals (unknown MAC addresses, active IPS/DPI/flood/scan/rogue alarms), and re-surfaces anything security-relevant the daily audit already found. Everything gets a severity from &lt;code&gt;ok&lt;/code&gt; up through &lt;code&gt;critical&lt;/code&gt;, and the result is written as an Obsidian note with YAML frontmatter. So a year of these becomes a searchable, taggable incident timeline instead of a folder of text files nobody opens.&lt;/p&gt;
&lt;h2 id="where-the-llm-fits---and-where-it-doesnt"&gt;Where the LLM fits - and where it doesn&amp;rsquo;t
&lt;/h2&gt;&lt;p&gt;Both agents can optionally hand their findings to a local Ollama model to write a short narrative summary on top of the facts. This is the part I was most careful about, because it&amp;rsquo;s the part most people get backwards.&lt;/p&gt;
&lt;p&gt;The model never sees raw logs, full inventories, or matched secrets. It only compact finding titles, IDs, severities, and evidence keys. It doesn&amp;rsquo;t decide what&amp;rsquo;s a finding; the deterministic analyzers do that. And if Ollama is unreachable or returns something unusable, the deterministic digest ships as-is. The LLM is a narrator, never the source of truth.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the same boundary I&amp;rsquo;d want on a production alerting pipeline: detection logic stays deterministic and testable, and generative summarization sits strictly downstream of it, never upstream.&lt;/p&gt;
&lt;h2 id="the-part-thats-just-operational-discipline"&gt;The part that&amp;rsquo;s just operational discipline
&lt;/h2&gt;&lt;p&gt;A few choices here have nothing to do with security theory and everything to do with habits from running things in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Output paths are permissioned, not just &amp;ldquo;private by convention.&amp;rdquo;&lt;/strong&gt; Reports get &lt;code&gt;0600&lt;/code&gt;, runtime directories &lt;code&gt;0700&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stale data is treated as a finding, not silently trusted.&lt;/strong&gt; Upstream reports older than a configured threshold are flagged rather than quietly re-surfaced as current.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notifications are a tap-through, not the whole story.&lt;/strong&gt; A push notification carries the headline and a link into the actual note - useful at a glance, but the record of truth lives in the note, not in a chat history that scrolls away.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything is replayable offline.&lt;/strong&gt; Both agents accept a fixture file in place of live collection, so I can test a new analyzer rule against a known input before it ever touches my real network.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-it-actually-buys-me"&gt;What it actually buys me
&lt;/h2&gt;&lt;p&gt;Mostly, it buys me the same thing it buys at work: I notice drift before it becomes an incident, instead of after. A container that quietly picked up a privileged flag, a port that got published wider than intended, an unfamiliar MAC address on the network. These are exactly the kind of small, boring facts that are easy to miss and easy to detect deterministically.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a real SOC. There&amp;rsquo;s no 24/7 coverage, no incident response retainer, and the threat model is &amp;ldquo;don&amp;rsquo;t get owned by something dumb,&amp;rdquo; not &amp;ldquo;defend against a motivated attacker.&amp;rdquo; But the muscle is the same one I use at work: write the check once, make it boring and deterministic, let a model help you read the output, and keep a record you can actually search six months later.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re already doing this professionally, the homelab version costs you an evening and pays you back the first time you catch something you would have otherwise missed entirely.&lt;/p&gt;</description></item></channel></rss>