<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rusty Bower</title><link>https://www.rustybower.com/</link><description>Recent content on Rusty Bower</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 21 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.rustybower.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Mac Studio as a Kubernetes Compute Satellite</title><link>https://www.rustybower.com/posts/mac-studio-kubernetes-compute-satellite/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/mac-studio-kubernetes-compute-satellite/</guid><description>&lt;p&gt;My K3s cluster runs on six Intel NUCs. They&amp;rsquo;re great for the 50+ containers that make up my homelab — Plex, Home Assistant, Frigate, Paperless, the usual suspects. But they&amp;rsquo;re terrible at machine learning. Four Skylake cores and 32 GB of RAM per node doesn&amp;rsquo;t get you far when Immich wants to classify 80,000 photos or Frigate wants a vision model to describe who&amp;rsquo;s at your door.&lt;/p&gt;
&lt;p&gt;The Mac Studio sitting under my desk — M1 Ultra, 64 GB unified memory — is the opposite problem. Absurd single-machine ML performance, but I don&amp;rsquo;t want to migrate my entire cluster to it. I want the K8s cluster to stay the control plane for routing, TLS, service discovery, and GitOps, while the Mac handles the heavy compute.&lt;/p&gt;</description></item><item><title>Automating a Smart Bird Feeder's Photo Archive with Kubernetes and Immich</title><link>https://www.rustybower.com/posts/birdbuddy-immich-photo-pipeline/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/birdbuddy-immich-photo-pipeline/</guid><description>&lt;p&gt;My &lt;a href="https://mybirdbuddy.com/"&gt;Bird Buddy&lt;/a&gt; smart feeder takes a photo every time a bird visits. Over a few months, that adds up to thousands of images — all sitting in Bird Buddy&amp;rsquo;s cloud app with no good way to search, organize, or back them up. I wanted them in &lt;a href="https://immich.app/"&gt;Immich&lt;/a&gt;, my self-hosted photo library, organized by species and properly timestamped.&lt;/p&gt;
&lt;p&gt;The Bird Buddy app doesn&amp;rsquo;t have an export feature. But the &lt;code&gt;pybirdbuddy&lt;/code&gt; Python library can authenticate to their API and walk the feed. So I built a two-stage pipeline: a CronJob that downloads new photos daily, and a second CronJob that syncs them into Immich. Both run in Kubernetes, and the whole thing is about 300 lines of Python.&lt;/p&gt;</description></item><item><title>Unit-Testing Home Automations That Run in Kubernetes</title><link>https://www.rustybower.com/posts/unit-tested-appdaemon-automations-kubernetes/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/unit-tested-appdaemon-automations-kubernetes/</guid><description>&lt;p&gt;Home automations are code that controls physical things. When they break, the consequences aren&amp;rsquo;t a 500 error — they&amp;rsquo;re a garage door that won&amp;rsquo;t close in a snowstorm, or window shades that open at 3 AM, or a tablet that charges to 100% and stays plugged in for months until the battery swells.&lt;/p&gt;
&lt;p&gt;I run about 20 &lt;a href="https://appdaemon.readthedocs.io/"&gt;AppDaemon&lt;/a&gt; automations in my homelab, all deployed as Kubernetes ConfigMaps and tested with &lt;a href="https://github.com/FlorianKemworthy/appdaemontestframework"&gt;appdaemontestframework&lt;/a&gt;. The testing isn&amp;rsquo;t academic — it&amp;rsquo;s caught real bugs that would have been annoying or expensive to discover in production.&lt;/p&gt;</description></item><item><title>Fixing 10,000 Upside-Down Scanned Slides with a Local Vision LLM</title><link>https://www.rustybower.com/posts/fixing-upside-down-scanned-slides-vision-llm/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/fixing-upside-down-scanned-slides-vision-llm/</guid><description>&lt;p&gt;My grandfather had about 10,000 35mm slides. I rented a &lt;a href="https://www.slidesnap.com/"&gt;SlideSnap X1&lt;/a&gt; and spent a weekend feeding them through — 33 boxes worth, organized into folders by box. The scanner itself was great, but when you&amp;rsquo;re pushing through thousands of slides in a weekend, some inevitably go in upside down or backwards. No metadata, no EXIF orientation flags — just thousands of JPEGs, some right-side up, some not, sitting on a NAS.&lt;/p&gt;</description></item><item><title>Finding Fraud in $1 Trillion of Medicaid Data with DuckDB</title><link>https://www.rustybower.com/posts/medicaid-fraud-detection-duckdb-227m-rows/</link><pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/medicaid-fraud-detection-duckdb-227m-rows/</guid><description>&lt;p&gt;CMS recently published provider-level Medicaid spending data from T-MSIS — every fee-for-service, managed care, and CHIP claim from 2018 through 2024, aggregated by billing provider, procedure code, and month. 227 million rows. $1.09 trillion in payments. I wanted to see what falls out when you run some basic fraud heuristics against it.&lt;/p&gt;
&lt;p&gt;The dataset is available at &lt;a href="https://opendata.hhs.gov/datasets/medicaid-provider-spending/"&gt;opendata.hhs.gov/datasets/medicaid-provider-spending/&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-dataset"&gt;The dataset&lt;/h2&gt;
&lt;p&gt;The download is a single 2.94 GB parquet file with seven columns:&lt;/p&gt;</description></item><item><title>Building a Home Assistant Dashboard with Whole Home Audio Control</title><link>https://www.rustybower.com/posts/home-assistant-dashboard-whole-home-audio/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/home-assistant-dashboard-whole-home-audio/</guid><description>&lt;p&gt;My Home Assistant dashboard has evolved significantly over the past year. What started as the default auto-generated cards has become a carefully organized interface that my whole family actually uses. Here&amp;rsquo;s how I built it, with a focus on the whole home audio system that ties everything together.&lt;/p&gt;
&lt;h2 id="dashboard-philosophy"&gt;Dashboard Philosophy&lt;/h2&gt;
&lt;p&gt;Before diving into implementation, a few principles guided the design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Function over flash&lt;/strong&gt; - Every card earns its screen space&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-tap actions&lt;/strong&gt; - Common tasks shouldn&amp;rsquo;t require drilling into menus&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status at a glance&lt;/strong&gt; - The overview tells you what&amp;rsquo;s happening without interaction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile-first&lt;/strong&gt; - Tablets mounted around the house are the primary interface&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-stack"&gt;The Stack&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lovelace YAML mode&lt;/strong&gt; - Full control over layout and structure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mushroom cards&lt;/strong&gt; - Clean, consistent UI components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Layout Card&lt;/strong&gt; - Responsive grid layouts that work on any screen&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack-in-card&lt;/strong&gt; - Grouping related controls together&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="overview-page"&gt;Overview Page&lt;/h2&gt;
&lt;p&gt;The overview page is designed to answer &amp;ldquo;what&amp;rsquo;s happening right now?&amp;rdquo; at a glance:&lt;/p&gt;</description></item><item><title>Auto-Populating Pi-hole DNS from Kubernetes Ingresses</title><link>https://www.rustybower.com/posts/external-dns-pihole-kubernetes/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/external-dns-pihole-kubernetes/</guid><description>&lt;p&gt;Managing DNS records for a homelab with dozens of services is tedious. Every time you deploy something new, you have to remember to add a DNS record. I solved this by using external-dns to automatically create Pi-hole DNS entries from Kubernetes ingress resources.&lt;/p&gt;
&lt;h2 id="the-goal"&gt;The Goal&lt;/h2&gt;
&lt;p&gt;When I create an ingress like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;networking.k8s.io/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Ingress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;frigate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external-dns.alpha.kubernetes.io/hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;frigate.bowerha.us&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;frigate.bowerha.us&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I want Pi-hole to automatically create a DNS record pointing &lt;code&gt;frigate.bowerha.us&lt;/code&gt; to my ingress controller&amp;rsquo;s IP. No manual steps, no forgetting to update DNS.&lt;/p&gt;</description></item><item><title>Building a Self-Updating Dashboard with Homepage and Kubernetes</title><link>https://www.rustybower.com/posts/homepage-kubernetes-service-discovery/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/homepage-kubernetes-service-discovery/</guid><description>&lt;p&gt;One of the challenges of running a homelab with dozens of services is keeping track of what&amp;rsquo;s running and where. I recently deployed &lt;a href="https://gethomepage.dev/"&gt;Homepage&lt;/a&gt; - a modern, fully static dashboard that automatically discovers services from Kubernetes ingresses.&lt;/p&gt;
&lt;h2 id="why-homepage"&gt;Why Homepage?&lt;/h2&gt;
&lt;p&gt;I evaluated several dashboard options including Homarr and Heimdall. Homepage stood out for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes-native service discovery&lt;/strong&gt; - no manual configuration needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time pod status&lt;/strong&gt; - shows if services are actually running&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean, modern UI&lt;/strong&gt; - dark theme, customizable layout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightweight&lt;/strong&gt; - just a static site with no database&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-setup"&gt;The Setup&lt;/h2&gt;
&lt;p&gt;Homepage runs as a simple deployment in Kubernetes with a ServiceAccount that has read access to the cluster. The magic happens through ingress annotations.&lt;/p&gt;</description></item><item><title>From Keel to Renovate: Better Container Image Updates for GitOps</title><link>https://www.rustybower.com/posts/keel-to-renovate-kubernetes-image-updates/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/keel-to-renovate-kubernetes-image-updates/</guid><description>&lt;p&gt;For years I used Keel to automatically update container images in my Kubernetes clusters. It worked, but as I moved to GitOps with ArgoCD, Keel&amp;rsquo;s push-based approach became a liability. I migrated to Renovate for PR-based image updates, and it&amp;rsquo;s been a significant improvement.&lt;/p&gt;
&lt;h2 id="the-problem-with-keel"&gt;The Problem with Keel&lt;/h2&gt;
&lt;p&gt;Keel watches for new container images and updates deployments directly in the cluster. You can configure it via annotations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;keel.sh/policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;major&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;keel.sh/trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poll&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a new image appears, Keel modifies the deployment in-place.&lt;/p&gt;</description></item><item><title>GitOps for Homelabs: Kustomize + ArgoCD Patterns and Pitfalls</title><link>https://www.rustybower.com/posts/kustomize-argocd-homelab-gitops/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/kustomize-argocd-homelab-gitops/</guid><description>&lt;p&gt;I manage two Kubernetes environments - a home cluster (bowerhaus) and a cloud cluster (rustycloud) - using GitOps with Kustomize and ArgoCD. After running this setup for a while, I&amp;rsquo;ve learned what works, what doesn&amp;rsquo;t, and some non-obvious gotchas.&lt;/p&gt;
&lt;h2 id="the-architecture"&gt;The Architecture&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kustomize/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── base/ # Shared, environment-agnostic configs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── media/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── lidarr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── radarr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── sonarr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── home-automation/
&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;│ │ └── frigate/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── data-analytics/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── prometheus/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── grafana/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── environments/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── bowerhaus/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── applicationsets/ # ArgoCD ApplicationSet
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── apps/ # Per-app overlays
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── frigate/
&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;│ │ └── prometheus/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── rustycloud/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── applicationsets/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── apps/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── plex/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── sonarr/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── grafana/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The key principle: &lt;strong&gt;base&lt;/strong&gt; contains environment-agnostic resources, &lt;strong&gt;environments&lt;/strong&gt; contain overlays that customize for each cluster.&lt;/p&gt;</description></item><item><title>Optimizing Z-Wave Thermostats for Home Assistant Control</title><link>https://www.rustybower.com/posts/zwave-thermostats-home-assistant/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/zwave-thermostats-home-assistant/</guid><description>&lt;p&gt;I have six Honeywell T6 Pro Z-Wave thermostats throughout my house controlling a multi-zone HVAC system. Out of the box, these thermostats have their own scheduling and &amp;ldquo;smart&amp;rdquo; features that conflict with Home Assistant automations. Here&amp;rsquo;s how I configured them to let HA take full control.&lt;/p&gt;
&lt;h2 id="the-setup"&gt;The Setup&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;6x Honeywell T6 Pro Z-Wave thermostats&lt;/strong&gt; (TH6320ZW2003)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z-Wave JS UI&lt;/strong&gt; running in Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt; with Z-Wave JS integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gas furnace + AC&lt;/strong&gt; (not heat pump) with multi-zone dampers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The zones:&lt;/p&gt;</description></item><item><title>Self-Hosted CI/CD with Drone, Gitea, and Harbor</title><link>https://www.rustybower.com/posts/drone-gitea-harbor-self-hosted-cicd/</link><pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.rustybower.com/posts/drone-gitea-harbor-self-hosted-cicd/</guid><description>&lt;p&gt;I run a fully self-hosted CI/CD pipeline using Drone for builds, Gitea for git hosting, and Harbor for container registry. No GitHub Actions, no Docker Hub, no external dependencies. Here&amp;rsquo;s how it all fits together.&lt;/p&gt;
&lt;h2 id="the-stack"&gt;The Stack&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gitea&lt;/strong&gt; - Lightweight git server with OAuth2 support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drone&lt;/strong&gt; - Container-native CI/CD platform&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harbor&lt;/strong&gt; - Enterprise container registry with vulnerability scanning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BuildKit&lt;/strong&gt; - Modern Docker builder for efficient image builds&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-self-hosted"&gt;Why Self-Hosted?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt; - Code never leaves my network&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No rate limits&lt;/strong&gt; - Build as often as needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline capability&lt;/strong&gt; - Works during internet outages (for local images)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning&lt;/strong&gt; - Understanding the full DevOps stack&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt; - No per-minute billing for CI runners&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&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;│ Kubernetes │
&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;│ ┌──────────┐ ┌─────────────────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Gitea │────▶│ Drone │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ (git) │ │ ┌───────┐ ┌──────────┐ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └──────────┘ │ │Server │ │ Runner │ │ │
&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;│ └────────────────────│────────┘ │
&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;│ ┌──────────┐ ┌──────▼─────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Harbor │◀──────────────────│ BuildKit │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │(registry)│ │ (builds) │ │
&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;│ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─────────────────────────────────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;Push code to Gitea&lt;/li&gt;
&lt;li&gt;Gitea webhook triggers Drone&lt;/li&gt;
&lt;li&gt;Drone spawns a build job via the Kubernetes runner&lt;/li&gt;
&lt;li&gt;BuildKit builds the container image&lt;/li&gt;
&lt;li&gt;Image is pushed to Harbor&lt;/li&gt;
&lt;li&gt;ArgoCD deploys the new image (separate workflow)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="gitea-setup"&gt;Gitea Setup&lt;/h2&gt;
&lt;p&gt;Gitea is straightforward - a single deployment with PostgreSQL backend. The key configuration is creating an OAuth2 application for Drone:&lt;/p&gt;</description></item></channel></rss>