<?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: Théo Verdiere</title>
    <description>The latest articles on DEV Community by Théo Verdiere (@c0rbz_tm).</description>
    <link>https://dev.to/c0rbz_tm</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3928532%2Fc47c3f65-5d24-4c4b-8cd5-f81ef4d03880.png</url>
      <title>DEV Community: Théo Verdiere</title>
      <link>https://dev.to/c0rbz_tm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/c0rbz_tm"/>
    <language>en</language>
    <item>
      <title>The three-line config I shelved, and the three-hour outage it became</title>
      <dc:creator>Théo Verdiere</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/c0rbz_tm/the-three-line-config-i-shelved-and-the-three-hour-outage-it-became-5a07</link>
      <guid>https://dev.to/c0rbz_tm/the-three-line-config-i-shelved-and-the-three-hour-outage-it-became-5a07</guid>
      <description>&lt;p&gt;For eight months, my k3s cluster did exactly nothing wrong.&lt;/p&gt;

&lt;p&gt;I'd installed it on a Hetzner VPS at the end of August: k3s, single node, the whole Grimoire stack on top (Postgres, Mongo, Elasticsearch, Keycloak, the backend). And it's not like I left it alone. I was in there all the time: reorganizing stacks, pulling pieces out and dropping new ones in, redeploying. The workloads churned constantly.&lt;/p&gt;

&lt;p&gt;What I never did, in eight months, was fully power the node down. The pods came and went; the host underneath, and the k3s server process on top of it, just stayed up. That distinction is the whole story. I just didn't know it yet.&lt;/p&gt;

&lt;p&gt;It didn't help that the box has 7.6 GB of RAM and zero swap, and was running like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;RAM (approx)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;k3s (apiserver, etcd, controllers)&lt;/td&gt;
&lt;td&gt;~1.2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;~1.5–2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kibana&lt;/td&gt;
&lt;td&gt;~500 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak&lt;/td&gt;
&lt;td&gt;~700 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL × 2&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;~300 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend (Spring Boot)&lt;/td&gt;
&lt;td&gt;~500 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traefik + cert-manager + system&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~5.5–7 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the cluster wasn't stable. It was sitting at the edge of its memory the entire time, and nothing had pushed it over yet. Eight months of uptime wasn't proof it was solid. It was just proof nothing had asked it a hard question.&lt;/p&gt;

&lt;p&gt;It also helped that I didn't have much riding on it. This was a staging cluster at the time: a sandbox I stood up to learn on, not production, not my paycheck, not something I was on call for. I never really worried about it going down, and that's exactly why I treated it the way I did: no swap, no alerts, the odd config note left for "later." Casually bolting a heavy metrics stack onto an already-full box was the same reflex, the kind of thing you do without much thought to a machine you're not afraid to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The morning it stopped being quiet
&lt;/h2&gt;

&lt;p&gt;There was no monitoring on this cluster, which is half the joke: the thing that finally tipped it over was me adding some.&lt;/p&gt;

&lt;p&gt;I'd just put the observability stack in: OpenTelemetry tracing on the backend, and the Grafana side in-cluster (Prometheus, Tempo, Loki). Useful, and also heavy, on a box already sitting near its ceiling. Memory went straight into the red. The only real lever I had to claw it back was Elasticsearch: by far the biggest single consumer, and the one thing I could shrink without dropping a feature. So I lowered its limits and went to restart it with the new values.&lt;/p&gt;

&lt;p&gt;That's when I found the cluster was already on the floor. My kube context kept dropping (commands hanging, the connection to the API server falling over and over), so I SSH'd into the box to look underneath. k3s was stuck in &lt;code&gt;activating (auto-restart)&lt;/code&gt;, looping. &lt;code&gt;free -m&lt;/code&gt; told the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               total        used        free      shared  buff/cache   available
Mem:            7634        7449         185          22         312         142
Swap:              0           0           0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;185 MB free, no swap to fall back on. The OOM killer had got there before I did, and one of the processes it had reached for was k3s itself. Not a pod, the server.&lt;/p&gt;

&lt;p&gt;I worked the rest of this out with an LLM open the entire time. I'm not a sysadmin, this is a layer I still know badly, and I navigated a good chunk of it by sight: pasting errors in, getting a direction, checking it against the actual state of the box. I learned a lot in three hours. I also guessed wrong more than once.&lt;/p&gt;

&lt;p&gt;The OOM, anyway, was the easy part. It was the trigger, not the whole outage. And a crash loop is k3s trying, over and over, to do the one thing it hadn't done in eight months: start cold. That's what was about to drag everything else into the light.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling it apart, one layer at a time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The token was gone.&lt;/strong&gt; k3s wouldn't come back, complaining it couldn't read its server token. I checked: &lt;code&gt;/var/lib/rancher/k3s/server/token&lt;/code&gt;, zero bytes. Empty. Best guess: an OOM kill landed mid-write during the crash loop and truncated it. k3s flatly refuses to start without that file. It &lt;em&gt;was&lt;/em&gt; recoverable from the &lt;code&gt;passwd&lt;/code&gt; credential file sitting right next to it. But I only know that now. At the time nothing told me, and a wiped token on a single-node cluster is the kind of thing that makes you briefly wonder if you're about to rebuild everything from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then IPv6 woke up.&lt;/strong&gt; Token restored, k3s tried to start, and threw a new error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dial tcp [::1]:6444: connect: connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one I'd been warned about. Back at install I'd half-registered that Hetzner always hands you dual-stack, IPv4 &lt;em&gt;and&lt;/em&gt; IPv6, and that k3s wants it configured explicitly. I'd filed it under &lt;em&gt;yeah, I should deal with that at some point&lt;/em&gt;, written nothing down, and never did. Eight months later, here it was.&lt;/p&gt;

&lt;p&gt;What happens is: &lt;code&gt;/etc/hosts&lt;/code&gt; maps &lt;code&gt;localhost&lt;/code&gt; to both &lt;code&gt;127.0.0.1&lt;/code&gt; and &lt;code&gt;::1&lt;/code&gt;, but the apiserver was only bound to &lt;code&gt;127.0.0.1:6444&lt;/code&gt;. So every internal component reaching for &lt;code&gt;localhost:6444&lt;/code&gt; ended up on &lt;code&gt;[::1]:6444&lt;/code&gt; and got refused.&lt;/p&gt;

&lt;p&gt;And it had been misconfigured the entire eight months. It never surfaced because the apiserver process had never restarted: as long as k3s was already up, nothing re-resolved that address. The bug needed a cold start to exist, and I'd never given the server one.&lt;/p&gt;

&lt;p&gt;My first fix made it worse: binding to &lt;code&gt;::1&lt;/code&gt; got the apiserver to pick the public IPv6 as its external host, which then clashed with the IPv4 service CIDR &lt;code&gt;10.43.0.0/16&lt;/code&gt;. The one that actually held, in &lt;code&gt;/etc/rancher/k3s/config.yaml&lt;/code&gt;:&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="na"&gt;kube-apiserver-arg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bind-address=::"&lt;/span&gt;                    &lt;span class="c1"&gt;# listen on both stacks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advertise-address=&amp;lt;public-ipv4&amp;gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# force IPv4 external host → matches the service CIDR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. That's the config from the title, the thing I'd shelved at install. Three lines, and the cluster would have cold-booted clean any day in those eight months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the zombies.&lt;/strong&gt; Every failed restart in the loop had left an orphaned &lt;code&gt;containerd-shim&lt;/code&gt; behind. By now there were fifteen-plus, jamming the next start. No clever fix: a full VPS reboot cleared them, and on the way back up &lt;code&gt;kubectl get nodes&lt;/code&gt; and &lt;code&gt;kubectl get pods -A&lt;/code&gt; finally came back clean. Roughly three hours, end to end.&lt;/p&gt;

&lt;p&gt;The whole chain, start to finish:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2025&lt;/td&gt;
&lt;td&gt;k3s installed: dual-stack, no swap. Two latent bugs ship with it, unnoticed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2025 → May 2026&lt;/td&gt;
&lt;td&gt;Eight months up. Pods churn constantly; the server process never restarts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May, ~07:00&lt;/td&gt;
&lt;td&gt;Observability stack added → memory into the red → OOM kills the k3s process → crash loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Server token truncated to 0 bytes mid-write → restored from the &lt;code&gt;passwd&lt;/code&gt; credential file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;First real cold start exposes the dormant IPv6 dual-stack bug → fixed in &lt;code&gt;config.yaml&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;15+ zombie &lt;code&gt;containerd-shim&lt;/code&gt; processes block restart → full VPS reboot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~10:00&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl get nodes&lt;/code&gt; returns clean. ~3 hours total.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why eight quiet months were the actual cause
&lt;/h2&gt;

&lt;p&gt;Step back and the OOM is almost a distraction. The two things that turned a process restart into a three-hour outage (the undocumented token recovery and the IPv6 binding) were both there on &lt;strong&gt;install day, in August.&lt;/strong&gt; Neither was introduced by the crash. They were just invisible until something forced the k3s server to start cold.&lt;/p&gt;

&lt;p&gt;Both lived in the same place: the gap between the k3s server going fully down and coming back up. Everything I'd done over those months happened above that line (pods cycling, stacks rearranged, Elasticsearch resized), and the server underneath never flinched, so neither bug ever showed. I never restarted it on purpose to check, because it never crossed my mind to; there was simply no question in my head to answer. One deliberate restart, or one reboot, on any calm afternoon would have surfaced both cold, with nothing on the line. Which is more or less what a staging box is there for. I just hadn't thought to use it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it left behind
&lt;/h2&gt;

&lt;p&gt;As usual, it was the kind of bug-chase that's annoying the whole way through and satisfying the moment it gives.&lt;/p&gt;

&lt;p&gt;It also left the system in better shape than it found it. The box has swap now, which covers the host and the k3s process itself. The pods still don't swap. The k3s token is backed up in Infisical. And the whole chain (OOM, token, IPv6, zombies) is written up as a post-mortem with recovery procedure, so if any of it comes back it's a checklist instead of three hours of guessing.&lt;/p&gt;

&lt;p&gt;That same box runs production now, so it's a good thing all of this came out while it was still a sandbox.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>linux</category>
    </item>
    <item>
      <title>Yet another personal project. I over-engineered it on purpose</title>
      <dc:creator>Théo Verdiere</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/c0rbz_tm/yet-another-personal-project-i-over-engineered-it-on-purpose-jkd</link>
      <guid>https://dev.to/c0rbz_tm/yet-another-personal-project-i-over-engineered-it-on-purpose-jkd</guid>
      <description>&lt;p&gt;It started one evening, out of the ordinary friction of the job: the organizational and budgetary constraints that come with any system at scale, and never enough time to keep things clean and pleasant to work on.&lt;/p&gt;

&lt;p&gt;Not a startup idea, not a side hustle with a monetization plan. Just a developer who'd spent a long time working inside systems he couldn't reshape, opening a fresh Kotlin project at 9pm because he wanted somewhere those limits didn't apply.&lt;/p&gt;

&lt;p&gt;So yes, this is &lt;em&gt;yet another personal project&lt;/em&gt;. The internet has no shortage of them. I knew from the start it would be oversized: a backend, an Android app, CI, infrastructure, a deployment. All for an audience of maybe six people. A bit of a champagne app on a beer budget.&lt;/p&gt;

&lt;p&gt;The overkill wasn't the surprise; that part was deliberate. What I didn't plan was building a &lt;em&gt;whole product&lt;/em&gt;. I started out just wanting to set up the infrastructure side myself, with something concrete running on it: a real frontend, a real backend, a real server.&lt;/p&gt;

&lt;p&gt;By day I work on systems that serve tens of thousands of concurrent users, and I'd only ever seen most of that machinery from inside a job. I wanted to take it apart myself, in close-to-production conditions, on something that was mine.&lt;/p&gt;

&lt;p&gt;It's been going for about 22 months now. What it became, and why I kept reaching for things it never needed, is the rest of this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  The product, and the reason
&lt;/h2&gt;

&lt;p&gt;Before going further, here's what all that infrastructure serves.&lt;/p&gt;

&lt;p&gt;Grimoire is a place to keep your recipes and how-to procedures, which it calls &lt;em&gt;Spells&lt;/em&gt;. Your Grimoire is private: the stuff you reach for every day. That's the part that's live today, Android-only, on the &lt;a href="https://play.google.com/store/apps/details?id=com.corbz.grimoire.android" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's wrapped in a cozy fantasy theme: Spells, a Grimoire, an Academy. I wanted it a bit geeky so it wouldn't feel like more work at 9pm. It's mobile-first (you cook, hands dirty, the phone's already on the counter) and deliberately slow: no feed, no nudges. You don't fill a Grimoire in a day.&lt;/p&gt;

&lt;p&gt;The rest is still being built. The Academy, a shared space where Spells get published, discovered and adapted: pull one into your own book, tweak it, share it back. A "lineage" view of how a published Spell gets forked downstream. Quests, for one-shot tasks. Guilds, smaller closed spaces to share within a group.&lt;/p&gt;

&lt;p&gt;That's the product. Most of it could have shipped on a single rented box. The app needed some of what follows; most of it I built because I wanted each layer in my own hands rather than borrowed from a job.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thesis
&lt;/h2&gt;

&lt;p&gt;A personal project with no deadline and no boss is the easiest place in the world to over-engineer. I've come to think that's not a flaw. It's the point.&lt;/p&gt;

&lt;p&gt;At work you ship the minimum: someone's paying, and it's not your time to gamble with. Strip that away and you can afford to try things and see what happens, with nothing lost if they don't pan out.&lt;/p&gt;

&lt;p&gt;So I did the unreasonable things on purpose. Hexagonal architecture, which later let me swap search engines in a weekend. Zero-downtime deploys on an Elasticsearch I didn't need yet. A pinch of DDD while the app was young enough to absorb it.&lt;/p&gt;

&lt;p&gt;You only learn what a pattern is worth once you've lived with it, even with an understanding as humble, and sometimes as shaky, as mine. What hexagonal and DDD actually looked like in this codebase is its own post.&lt;/p&gt;

&lt;p&gt;None of this is advice: having no users and no revenue let me make trade-offs I couldn't defend at work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Growing it one plateau at a time
&lt;/h2&gt;

&lt;p&gt;I never planned this as a stack. It grew in plateaus, each opened only once the previous one felt solid, and usually because I'd hit a wall that made the next layer worth building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felxo88c0b8rgujek65f0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felxo88c0b8rgujek65f0.png" alt="Architecture overview of Grimoire: Android app and web admin, backend, datastores, k3s on a Hetzner VPS" width="799" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend, alone, for three months.&lt;/strong&gt; December 2024 to March 2025: just Kotlin and Spring Boot. I used the time to sit properly with hexagonal architecture and a contract-first OpenAPI spec. No client yet, just the shape of the system.&lt;/p&gt;

&lt;p&gt;It was also where I really learned Kotlin and Gradle, and came to like how little Kotlin asks of you to get something clean working. I made my share of design mistakes in there too; some I only saw later, once a second layer started leaning on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it got a face.&lt;/strong&gt; March 2025: Android with Jetpack Compose, built alongside the backend. Up to then the app had lived as a wall of requests in Insomnia; this is where it stopped being an abstraction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pipeline, earlier than I needed one.&lt;/strong&gt; Around the same time, Dagger.io for CI: pipelines as real code, not YAML. Over-built in places, no question. But never wasted: it's what I run every day now, without thinking about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure, in a sandbox first.&lt;/strong&gt; Summer 2025: Pulumi, but on a local KinD cluster, not a live server. The same principle I lean on everywhere: the local environment should look as much like the real one as possible. So I got the deployment working locally before trusting it anywhere real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finally, a real server.&lt;/strong&gt; August 28th, 2025: a Hetzner VPS running k3s. I did this the old way, on a deliberately bare box: cheap, and empty. Everything a managed platform hides was now mine to stand up by hand: reverse proxy, cert manager, the lot. The point wasn't hardship for its own sake. A managed control plane lets you stay theoretical about that whole sysadmin layer, and I wanted to actually meet it. (It did break, spectacularly, eight months later. The post-mortem gets its own write-up.)&lt;/p&gt;

&lt;p&gt;From there it kept growing, driven by friction rather than a roadmap: Elasticsearch eating my VPS pushed me to Meilisearch; and since Spells are meant to be shared publicly, I built a small React admin to moderate them before opening that up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it didn't go to plan
&lt;/h2&gt;

&lt;p&gt;Deliberate overkill was only half of it. Plenty of the project came from the other direction: plans that met reality and bent.&lt;/p&gt;

&lt;p&gt;I set up a full Karate E2E suite early, expecting to lean on it; later I admitted it was the wrong call for a project this size and tore it out. I ran three separate repos for a year, sure it was cleaner, until the workflow got painful enough to push me into a monorepo. Neither was a master plan. Both were course corrections, one from hindsight, the other from sheer fatigue. (The monorepo move gets a full write-up later in the series.)&lt;/p&gt;

&lt;p&gt;Some of it was just blind spots. My backend habits had hidden a plain front-end fact from me: config gets frozen into the artifact at build time. It isn't read from the environment at startup the way it would be on a server. On mobile that's final; it ships embedded in the app. On web I could claw it back with a tiny Express server that serves the config at runtime. Either way I only hit it at deploy time, on the part of the project I'd filed under "the easy corner."&lt;/p&gt;

&lt;p&gt;The landing page is the clearest case. It wasn't planned at all: I just needed somewhere to host the terms of service. I built a real one anyway, because I felt like it, but kept it cheap on purpose. Astro, pushed to Cloudflare Pages, built locally, done. No Pulumi, no Dagger pipeline. A small static site wasn't worth the maintenance, time and learning the backend was.&lt;/p&gt;

&lt;p&gt;Wrapping it in the same machinery would have been absurd. (Cloudflare is exactly the managed, single-vendor thing I spent the whole infra effort pushing against. But a static site is portable &lt;em&gt;because&lt;/em&gt; it's trivial: I can move it in an afternoon. I'll dig into where "provider-agnostic" actually holds, and where it doesn't, in a separate post.)&lt;/p&gt;

&lt;p&gt;So the overkill dial wasn't set once and left there. It moved with what the project threw at me and what I had the energy for: studied on the backend, deliberately minimal on the landing page, and now and then just whatever I could be bothered with that month.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;So where is all this now? The app is live, actually in production on a €10-a-month VPS, and it's been there a few weeks rather than sitting behind a "coming soon." A handful of friends and colleagues are the first real users. You can &lt;a href="https://play.google.com/store/apps/details?id=com.corbz.grimoire.android" rel="noopener noreferrer"&gt;get it on the Play Store&lt;/a&gt;, or have a look at the &lt;a href="https://landing.api-grimoire.ovh/" rel="noopener noreferrer"&gt;landing page&lt;/a&gt;. The app, the CI toolkit and the landing page are public on &lt;a href="https://gitlab.com/grimoire-project" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;; the infrastructure repo stays private, since it describes a cluster that's running.&lt;/p&gt;

&lt;p&gt;Building the whole thing myself, from backend to cluster to landing page, is what finally made a system click for me, after years of only operating one slice of it. The rest is too specific for one post, so it's scattered across smaller ones: the k3s crash, Pulumi's portability promise, hexagonal and DDD in practice, the search-engine swap, where I drew the line with Keycloak, and the monorepo I backed into.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The three-line config I shelved, and the three-hour outage it became &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Building my infrastructure like I build software &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Everyone has an opinion about DDD. I'd never had my own. &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;I swapped my whole search engine in a weekend. The domain never noticed. &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;What I let Keycloak handle, and what I took back &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The API client I published and pulled, until a monorepo just built it &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sideprojects</category>
      <category>kotlin</category>
      <category>kubernetes</category>
      <category>android</category>
    </item>
  </channel>
</rss>
