<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Steven J. Vik</title>
    <description>The latest articles on DEV Community by Steven J. Vik (@stevenjvik).</description>
    <link>https://dev.to/stevenjvik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3831709%2Fb438618e-1010-4e61-af60-2c8b6ccad019.png</url>
      <title>DEV Community: Steven J. Vik</title>
      <link>https://dev.to/stevenjvik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stevenjvik"/>
    <language>en</language>
    <item>
      <title>How I Added SIEM to My Homelab With Wazuh — and What It Found on Day One</title>
      <dc:creator>Steven J. Vik</dc:creator>
      <pubDate>Wed, 01 Apr 2026 15:58:11 +0000</pubDate>
      <link>https://dev.to/stevenjvik/how-i-added-siem-to-my-homelab-with-wazuh-and-what-it-found-on-day-one-cll</link>
      <guid>https://dev.to/stevenjvik/how-i-added-siem-to-my-homelab-with-wazuh-and-what-it-found-on-day-one-cll</guid>
      <description>&lt;p&gt;I've been running Grafana and Prometheus on my homelab for about a year. CPU usage, RAM, disk, container uptime — the usual infrastructure metrics. I thought that was monitoring.&lt;/p&gt;

&lt;p&gt;Then I deployed Wazuh and found out I had no idea what was happening on my network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Wazuh Is
&lt;/h2&gt;

&lt;p&gt;Wazuh is an open-source SIEM (Security Information and Event Management) and XDR platform. It collects logs and events from agents you deploy on your systems, runs them through detection rules, and alerts you when something looks wrong. It's the same class of tool that security teams use in production environments — and it's free.&lt;/p&gt;

&lt;p&gt;The key mental model: Grafana asks "is this working?" Wazuh asks "is this being abused?" You need both questions answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I'm running a 3-node Proxmox VE 8.x cluster. Wazuh 4.9.2 all-in-one lives in LXC 107 on my main node (nx-core-01). Container specs: 4 vCPU, 8GB RAM, 50GB disk. Wazuh is memory-hungry — don't go below 6GB or the indexer will struggle.&lt;/p&gt;

&lt;p&gt;The all-in-one install script handles the indexer, server, and dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sO&lt;/span&gt; https://packages.wazuh.com/4.9/wazuh-install.sh
bash wazuh-install.sh &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 15 minutes. Dashboard on HTTPS port 443. Default credentials are in &lt;code&gt;/home/admin/.wazuh-install-files/wazuh-passwords.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One gotcha for LXC: Wazuh needs to read system audit logs. Some LXC security profiles block this. If ossec-logcollector can't read &lt;code&gt;/var/log/audit/audit.log&lt;/code&gt;, check your container's capability settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enrolling Agents
&lt;/h2&gt;

&lt;p&gt;I enrolled 7 agents: the main Proxmox node plus the LXC containers I care most about — the web server, Traefik, the monitoring stack, uptime-kuma, and Garrison (my AI orchestrator).&lt;/p&gt;

&lt;p&gt;Per-agent on each host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://packages.wazuh.com/key/GPG-KEY-WAZUH | apt-key add -
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://packages.wazuh.com/4.x/apt/ stable main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/wazuh.list
apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install &lt;/span&gt;wazuh-agent

&lt;span class="nv"&gt;WAZUH_MANAGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;wazuh-ip&amp;gt;"&lt;/span&gt; &lt;span class="nv"&gt;WAZUH_AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; wazuh-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Custom Detection Rules
&lt;/h2&gt;

&lt;p&gt;Wazuh ships with thousands of built-in rules. I added custom rules for things specific to my setup in &lt;code&gt;/var/ossec/etc/rules/local_rules.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- SSH brute force — T1110 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;rule&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"100001"&lt;/span&gt; &lt;span class="na"&gt;level=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;if_group&amp;gt;&lt;/span&gt;syslog&lt;span class="nt"&gt;&amp;lt;/if_group&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;match&amp;gt;&lt;/span&gt;pam_unix.*authentication failure&lt;span class="nt"&gt;&amp;lt;/match&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;same_source_ip&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;frequency&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/frequency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;timeframe&amp;gt;&lt;/span&gt;120&lt;span class="nt"&gt;&amp;lt;/timeframe&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Multiple SSH auth failures from same IP&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mitre&amp;gt;&amp;lt;id&amp;gt;&lt;/span&gt;T1110&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&amp;lt;/mitre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Root SSH login — should never happen --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;rule&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"100002"&lt;/span&gt; &lt;span class="na"&gt;level=&lt;/span&gt;&lt;span class="s"&gt;"15"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;if_sid&amp;gt;&lt;/span&gt;5715&lt;span class="nt"&gt;&amp;lt;/if_sid&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;match&amp;gt;&lt;/span&gt;^Accepted.*root@&lt;span class="nt"&gt;&amp;lt;/match&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Root login via SSH detected&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mitre&amp;gt;&amp;lt;id&amp;gt;&lt;/span&gt;T1078&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&amp;lt;/mitre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mapping rules to MITRE ATT&amp;amp;CK IDs is worth the extra minute. It surfaces in the Wazuh dashboard and makes it easier to reason about what class of attack you're detecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email Alerts
&lt;/h2&gt;

&lt;p&gt;Wazuh can send email alerts directly. I use msmtp as a relay with a Gmail app password. In &lt;code&gt;ossec.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;email_notification&amp;gt;&lt;/span&gt;yes&lt;span class="nt"&gt;&amp;lt;/email_notification&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;smtp_server&amp;gt;&lt;/span&gt;localhost&lt;span class="nt"&gt;&amp;lt;/smtp_server&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;email_from&amp;gt;&lt;/span&gt;wazuh@homelab.local&lt;span class="nt"&gt;&amp;lt;/email_from&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;email_to&amp;gt;&lt;/span&gt;you@yourdomain.com&lt;span class="nt"&gt;&amp;lt;/email_to&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;email_maxperhour&amp;gt;&lt;/span&gt;12&lt;span class="nt"&gt;&amp;lt;/email_maxperhour&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;email_alert_level&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/email_alert_level&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Level 10+ fires an email. Root login would be level 15 — that's a middle-of-the-night wake-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Found on Day One
&lt;/h2&gt;

&lt;p&gt;This is the part that made me feel like I'd been flying blind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH brute-force from 4 external IPs.&lt;/strong&gt; This was ongoing. My SSH is on the default port (yes, I know — now I've moved it) and there were continuous auth attempts from IPs in Ukraine, China, and Romania. None of this was in Grafana. It wasn't breaking anything, but I wasn't informed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A container service in a restart loop.&lt;/strong&gt; One of my LXC containers had a service that was failing and restarting every few minutes. The container showed as "running" in Uptime Kuma. The service itself was not. Wazuh caught it in the syslog stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two containers with outdated packages.&lt;/strong&gt; Wazuh's rootcheck module audits package state. Both flagged containers had packages with known CVEs. Neither was critical, but I wouldn't have known without a scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana + Wazuh: Better Together
&lt;/h2&gt;

&lt;p&gt;This isn't an either/or. Grafana tells me if my services are healthy. Wazuh tells me if my systems are being probed, if accounts are being misused, if files are changing unexpectedly.&lt;/p&gt;

&lt;p&gt;The combined setup is a proper monitoring stack for a homelab that's exposed to the internet — even partially.&lt;/p&gt;

&lt;p&gt;If you're running self-hosted infrastructure and haven't added a SIEM layer, Wazuh is the most accessible path to getting there. The all-in-one install makes it genuinely feasible without a dedicated security team.&lt;/p&gt;

&lt;p&gt;I've documented the full Proxmox cluster setup, monitoring stack, and security hardening approach at &lt;a href="https://sjvik-labs.stevenjvik.tech/guides" rel="noopener noreferrer"&gt;sjvik-labs.stevenjvik.tech/guides&lt;/a&gt; if you want to go deeper.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>cybersecurity</category>
      <category>selfhosted</category>
      <category>linux</category>
    </item>
    <item>
      <title>I ran incident response on my own homelab. Here's the postmortem.</title>
      <dc:creator>Steven J. Vik</dc:creator>
      <pubDate>Wed, 01 Apr 2026 15:57:07 +0000</pubDate>
      <link>https://dev.to/stevenjvik/i-ran-incident-response-on-my-own-homelab-heres-the-postmortem-366h</link>
      <guid>https://dev.to/stevenjvik/i-ran-incident-response-on-my-own-homelab-heres-the-postmortem-366h</guid>
      <description>&lt;p&gt;I run a 3-node Proxmox cluster at home with 11 LXC containers. Last week one of them turned into an incident.&lt;/p&gt;

&lt;p&gt;Not a dramatic one. No data loss. No outage that affected anyone else. But it hit the same failure modes I see documented in enterprise postmortems — and handling it the same way taught me more than any homelab YouTube video has.&lt;/p&gt;

&lt;p&gt;Here's what happened and what I changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The incident
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;00:47&lt;/strong&gt; — My homelab control panel stops responding. The web UI that ties together monitoring, service status, and agent health is down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;00:47–01:09&lt;/strong&gt; — PM2 restarts the service. Then restarts it again. 32 times total, with exponential backoff, over about 22 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;01:09&lt;/strong&gt; — Prometheus alert fires. Wazuh catches the anomaly in PM2 process metrics. I get paged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;01:11&lt;/strong&gt; — I SSH in. &lt;code&gt;pm2 logs sjvik-control-panel&lt;/code&gt; shows the immediate cause: &lt;code&gt;Cannot find module tsx&lt;/code&gt;. The package is gone from node_modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;01:13&lt;/strong&gt; — &lt;code&gt;npm install &amp;amp;&amp;amp; pm2 restart sjvik-control-panel&lt;/code&gt;. Service is back.&lt;/p&gt;

&lt;p&gt;Total downtime from first failure to recovery: 26 minutes. Time from alert to recovery: 4 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The postmortem
&lt;/h2&gt;

&lt;p&gt;If you've done incident response at work, this format will look familiar. I use it at home too — not because it's bureaucratic overhead, but because it forces honest thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What failed:&lt;/strong&gt; &lt;code&gt;tsx&lt;/code&gt; missing from node_modules on LXC 101 (nx-web-01). Root cause unknown — most likely a stale node_modules state after a system update or partial &lt;code&gt;npm ci&lt;/code&gt; run that didn't complete cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What detected it:&lt;/strong&gt; External monitoring (Prometheus + pm2-prometheus-exporter tracking restart counts per process). NOT the service itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What slowed detection:&lt;/strong&gt; The 22-minute gap between first failure and alert. PM2's default backoff delays mean a fast-dying service doesn't trip alerts immediately. I had no alert threshold on restart &lt;em&gt;rate&lt;/em&gt; — only on sustained high restart counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What enabled fast recovery:&lt;/strong&gt; The service is stateless. No data to recover. &lt;code&gt;npm install&lt;/code&gt; is idempotent. Recovery procedure existed in muscle memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't exist:&lt;/strong&gt; A startup health check that validates dependencies &lt;em&gt;before&lt;/em&gt; PM2 marks the service alive. The service was failing fast, not failing safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Prestart dependency validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Added to &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"prestart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm ls --depth=0 --silent || (echo 'Dependency validation failed — run npm install' &amp;amp;&amp;amp; exit 1)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now on restart, PM2 gets a clean exit with an actionable message. Restart 1 tells me what's wrong. Not restart 32.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Alert on restart &lt;em&gt;rate&lt;/em&gt;, not just count&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Updated the Prometheus alert rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PM2ServiceRestartRateSpiking&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate(pm2_restarts_total[5m]) &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2m&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;restarting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;frequently"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fires within 2 minutes of a sustained restart loop instead of waiting for a count threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Runbook entry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I added a one-page runbook to my Obsidian vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service: sjvik-control-panel
Recovery: cd /root/projects/sjvik-control-panel &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; pm2 restart sjvik-control-panel
Verify: curl -s http://localhost:3456/health | jq .status
Escalate if: health endpoint returns non-200 after npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runbooks feel like overkill for a homelab until 2am when you're tired and need the answer fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why homelab IR matters
&lt;/h2&gt;

&lt;p&gt;The enterprise version of this incident would involve a runbook, a Slack war room, a timeline in PagerDuty, and a written postmortem shared with the team. The homelab version is: you fixing something alone at 1am with nobody watching.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;thinking&lt;/em&gt; is the same.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detection time matters. Alert on behavior, not just state.&lt;/li&gt;
&lt;li&gt;Recovery time improves with runbooks. Write them while you still remember what you did.&lt;/li&gt;
&lt;li&gt;Postmortems find gaps that didn't feel like gaps until something broke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have 11 containers. At least a few of them will have incidents. Treating each one as a real IR exercise is how I actually get better at this — not just at homelab ops, but at the SOC analyst work I do professionally.&lt;/p&gt;

&lt;p&gt;The next time I'm in an enterprise war room following an incident response process, I'll have done it 15 times already on my own infrastructure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running a homelab security stack? My &lt;a href="https://stevenjvik.tech/guides" rel="noopener noreferrer"&gt;Proxmox Homelab guide&lt;/a&gt; covers the full setup — Proxmox cluster, PBS backups, Wazuh SIEM, and the PM2 service patterns that kept this recoverable in under 5 minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>devops</category>
      <category>security</category>
      <category>linux</category>
    </item>
    <item>
      <title>I Ran 7 Autonomous AI Agents on My Homelab Proxmox Cluster — Here's What Actually Happened</title>
      <dc:creator>Steven J. Vik</dc:creator>
      <pubDate>Wed, 18 Mar 2026 16:08:10 +0000</pubDate>
      <link>https://dev.to/stevenjvik/i-ran-7-autonomous-ai-agents-on-my-homelab-proxmox-cluster-heres-what-actually-happened-43ka</link>
      <guid>https://dev.to/stevenjvik/i-ran-7-autonomous-ai-agents-on-my-homelab-proxmox-cluster-heres-what-actually-happened-43ka</guid>
      <description>&lt;p&gt;I've been building homelab infrastructure seriously for a few years — 3-node Proxmox cluster, 13 LXC containers, self-hosted everything. But last month I did something different: I deployed a self-hosted AI agent orchestration platform and gave it a task list. Seven autonomous agents, each waking on a schedule, each writing back to my Obsidian vault, each filing issues when something breaks.&lt;/p&gt;

&lt;p&gt;Here's what I actually learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The cluster: 3 nodes named "nexus." nx-core-01 handles most services (64GB RAM), nx-ai-01 runs Ollama for local inference (32GB, NVMe), nx-store-01 handles Samba shares and Proxmox Backup Server. All on Proxmox VE 8.x with corosync clustering.&lt;/p&gt;

&lt;p&gt;The agents run in LXC 109 on nx-core-01, using a platform called Paperclip. Each agent has a &lt;code&gt;HEARTBEAT.md&lt;/code&gt; — a plain-text checklist it follows every time it wakes up. That's it. No complex prompt engineering. Just: "Here's who you are. Here's what to check. Write the results here."&lt;/p&gt;

&lt;p&gt;The seven agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SRE&lt;/strong&gt; (every 3h): SSHes into all nodes, checks service health, files an issue if something is down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt; (daily): Reads my Obsidian vault for recent lab work, drafts LinkedIn/Reddit posts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Career&lt;/strong&gt; (daily): Queries my job-tracker SQLite DB, ranks opportunities by fit, summarizes to daily notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps&lt;/strong&gt; (daily): Audits GitHub repos, checks CI status, verifies nightly git pushes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; (daily): Queries Prometheus + Grafana, writes a daily metrics digest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product&lt;/strong&gt; (daily): Audits Gumroad products, checks for new reviews or feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comms&lt;/strong&gt; (every 12h): Triages Gmail via Google Workspace API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Worked Immediately
&lt;/h2&gt;

&lt;p&gt;The SRE agent was the most reliable from day one. On the first run, it SSHed into all three nodes, pulled service status, noticed that LXC 107 (twingate-connector) was stopped, and filed a Paperclip issue with the container ID and status output. I didn't tell it about twingate. It just found it in the container list, noticed it was stopped while everything else was running, and flagged it.&lt;/p&gt;

&lt;p&gt;The career tracker integration was the pleasant surprise. I have a SQLite database of job postings I've scraped over time. The agent queries it with something like &lt;code&gt;SELECT * FROM jobs WHERE applied = 0 ORDER BY match_score DESC LIMIT 10&lt;/code&gt;, formats the top results into a readable summary, and writes it to my daily note. It turned a manual process I was doing twice a week into something that happens automatically — and surfaces opportunities I might have scrolled past.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Needed Fixing
&lt;/h2&gt;

&lt;p&gt;Session management was the first real problem.&lt;/p&gt;

&lt;p&gt;LLM-based agents maintain conversational context across turns. That's useful for a single session, but in a heartbeat model — where the agent wakes up, does work, and goes to sleep — stale context is actively harmful. An agent that "remembered" it already checked disk health yesterday would skip the check today. An agent that "remembered" a task was in-progress would try to continue work that had already been resolved.&lt;/p&gt;

&lt;p&gt;The fix: clear session IDs whenever agent instructions change. A fresh context every heartbeat. Stateless by design.&lt;/p&gt;

&lt;p&gt;The second problem was race conditions. Two agents would both see an unhandled task and try to work it simultaneously. The checkout system in Paperclip handles this (first agent to POST &lt;code&gt;/checkout&lt;/code&gt; wins, second gets a 409), but I had to make sure agents honored the 409 and moved on instead of retrying.&lt;/p&gt;

&lt;p&gt;The third problem was scope creep. Agents without tight constraints would start doing "helpful" things outside their mandate — refactoring files they weren't asked to touch, commenting on issues that weren't assigned to them. The fix was explicit constraints in the HEARTBEAT.md: &lt;code&gt;MAY&lt;/code&gt; and &lt;code&gt;NEVER&lt;/code&gt; blocks that spell out what the agent can and can't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Assessment
&lt;/h2&gt;

&lt;p&gt;This is not magic. It's infrastructure work with an LLM attached.&lt;/p&gt;

&lt;p&gt;The agents don't reason in any interesting way — they follow instructions and fill in the gaps with pattern matching. They're good at: structured tasks with clear outputs, pulling data from known sources, writing formatted summaries, and catching obvious anomalies.&lt;/p&gt;

&lt;p&gt;They're not good at: novel situations, tasks that require deep context they don't have, or anything where "good enough" isn't good enough.&lt;/p&gt;

&lt;p&gt;What's running reliably now: daily SRE health checks, content drafts, job alert summaries, GitHub audits. These are all tasks that were already defined processes — the agents just execute them faster and without me having to remember to do them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Infrastructure Mindset Shift
&lt;/h2&gt;

&lt;p&gt;The thing that helped most was stopping thinking of these as "AI assistants" and starting thinking of them as distributed processes with unreliable executors.&lt;/p&gt;

&lt;p&gt;You don't debug an agent by asking it what went wrong. You look at its output, trace the failure back to the instruction that produced it, and fix the instruction. Same as debugging any script.&lt;/p&gt;

&lt;p&gt;You don't trust an agent's memory. You design the system so memory doesn't matter — idempotent operations, fresh context per run, explicit state in files rather than agent recall.&lt;/p&gt;

&lt;p&gt;You don't scale by adding intelligence. You scale by tightening the feedback loop — better outputs → better prompts → tighter heartbeats.&lt;/p&gt;

&lt;p&gt;If you're building self-hosted infrastructure and want to go deeper on the cluster setup, monitoring stack, and LXC templates that underpin all of this — I've documented it in full at &lt;a href="https://sjvik-labs.stevenjvik.tech/guides" rel="noopener noreferrer"&gt;sjvik-labs.stevenjvik.tech/guides&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about specific components in the comments — Traefik config, PBS setup, Prometheus scrape targets, or the agent architecture.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>proxmox</category>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
