<?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: Anna Silva</title>
    <description>The latest articles on DEV Community by Anna Silva (@notjustanna).</description>
    <link>https://dev.to/notjustanna</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%2F534773%2Ff39757bc-9700-47f0-9394-4cd6ff215379.png</url>
      <title>DEV Community: Anna Silva</title>
      <link>https://dev.to/notjustanna</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/notjustanna"/>
    <language>en</language>
    <item>
      <title>Astro is Great, Actually</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/notjustanna/astro-is-great-actually-5e6g</link>
      <guid>https://dev.to/notjustanna/astro-is-great-actually-5e6g</guid>
      <description>&lt;p&gt;I've been building personal websites long enough to have opinions about Bootstrap 2. Not nostalgia — opinions. It was the right tool for 2013, it held its ground on IE, and if you think that's funny you've never debugged a flexbox fallback at 1am for a browser that predates flexbox.&lt;/p&gt;

&lt;p&gt;Since then: CRA when I wanted to play with the fancy React hooks, Next.js when Next.js was the new hotness, Tailwind the moment I learned it existed (and it never left), Vite when I wanted a cleaner foundation. The through-line is Tailwind and &lt;code&gt;.tsx&lt;/code&gt;. That's where I live. Everything else is negotiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Wanted to Write Things Down
&lt;/h2&gt;

&lt;p&gt;Dev.to was the obvious first move. It's where dev content lives, the tooling is fine, the audience is there. So I started writing there.&lt;/p&gt;

&lt;p&gt;And then I wanted to write about other things. Keyboards. Thoughts that don't resolve into a tutorial. Life, vaguely. Dev.to technically allows it — some people are fine with that — but it never felt right. Dev.to is for dev content. Personal stuff belongs somewhere personal.&lt;/p&gt;

&lt;p&gt;Which meant I needed a blog. Which meant I needed blog plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Obvious Problem
&lt;/h2&gt;

&lt;p&gt;My portfolio site was just Vite. Static. No backend, no CMS, no server doing anything interesting. "Just add a blog" implies some infrastructure I don't have.&lt;/p&gt;

&lt;p&gt;Someone said: Astro.&lt;/p&gt;




&lt;h2&gt;
  
  
  Astro the Framework
&lt;/h2&gt;

&lt;p&gt;Astro is a static site generator built on top of Vite. Which, first of all, COOL. Migration from my existing setup would be mostly renaming things. But the actually interesting part: Astro has a component model where you can drop in React, Svelte, Vue — it handles hydration for whichever ones need it, leaving everything else as zero-JS static HTML. The integrations list is absurd. MDX out of the box. Image optimization. RSS feeds. Sitemaps. Content collections with schema validation. It's not opinionated about what you're building; it just handles the boring parts so you don't have to.&lt;/p&gt;

&lt;p&gt;Okay. I'm interested. What's the catch?&lt;/p&gt;




&lt;h2&gt;
  
  
  Astro the Language
&lt;/h2&gt;

&lt;p&gt;The catch is &lt;code&gt;.astro&lt;/code&gt; files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import { getCollection } from 'astro:content';
import { PostList } from '../components/blog/post-list.astro';
import { BaseLayout } from '../layouts/base-layout.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';

const posts = (await getCollection('posts')).sort(
  (a, b) =&amp;gt; b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---

&amp;lt;BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}&amp;gt;
  &amp;lt;section&amp;gt;
    &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;PostList posts={posts} /&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;/BaseLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I looked at them and went: ugh. Frontmatter at the top, template below, logic mixed in — it gave me flashbacks to the Jekyll era of GitHub Pages, which itself gave me flashbacks to PHP files opening with a cascading reverse-indented avalanche of &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;s from the template three includes up. You know the files I'm talking about.&lt;/p&gt;

&lt;p&gt;What made React work for me was precisely that it isn't that. A component is a function that returns HTML — not &lt;em&gt;really&lt;/em&gt; HTML, it's JSX, it's movie magic — but the DOM is being treated as an object, not a text file with &lt;code&gt;&amp;lt;?php echo&lt;/code&gt; stitched into it. I did not get into React to write PHP again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Oh, I Can Just Keep Using React
&lt;/h2&gt;

&lt;p&gt;Turns out: yes.&lt;/p&gt;

&lt;p&gt;My portfolio could stay in React and &lt;code&gt;.tsx&lt;/code&gt;. The blog content is entirely Markdown with frontmatter. The blog's theming could also be fully written in React and &lt;code&gt;.tsx&lt;/code&gt; as well. The &lt;code&gt;.astro&lt;/code&gt; files handle the parts that would be awkward in JSX anyway — the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; shell, path handlers, layout wrappers. Everything with actual logic is still React. Still a function that returns HTML.&lt;/p&gt;

&lt;p&gt;Which left me with one question: what actually &lt;em&gt;are&lt;/em&gt; &lt;code&gt;.astro&lt;/code&gt; files?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lingua Franca
&lt;/h2&gt;

&lt;p&gt;They read like a PHP template. They parse more like Vue or Svelte with frontmatter. They have the structural feel of a Jekyll theme. It's as if someone looked at the entire history of web templating, said "yes, all of it," and produced something simultaneously familiar and alien depending on which corner you're looking at.&lt;/p&gt;

&lt;p&gt;The generous read — and I think it's the correct one — is that &lt;code&gt;.astro&lt;/code&gt; is the lingua franca of frontend templating. English absorbed Latin, German, French, Spanish, and somehow became a language. Not clean, not internally consistent, but widely legible. &lt;code&gt;.astro&lt;/code&gt; files are like that. Not everyone's cup of tea. Definitely not mine, initially.&lt;/p&gt;

&lt;p&gt;But once I understood what layer they operate at, the strangeness stopped mattering. The PHP comparison doesn't hold. It just looks that way from a distance.&lt;/p&gt;




&lt;p&gt;The blog works. The whole thing is fast, the build tooling is familiar, and at no point did Astro ask me to change how I write components.&lt;/p&gt;

&lt;p&gt;That last part is the actual endorsement. Frameworks have opinions. Astro had opinions too, and none of them got in my way. That's impressive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@jayphoto" rel="noopener noreferrer"&gt;Justin Wolff&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>react</category>
    </item>
    <item>
      <title>Design Constraints as Art: Maximizing Your AWS Free Tier</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Fri, 03 Apr 2026 00:05:16 +0000</pubDate>
      <link>https://dev.to/notjustanna/design-constraints-as-art-maximizing-your-aws-free-tier-59m8</link>
      <guid>https://dev.to/notjustanna/design-constraints-as-art-maximizing-your-aws-free-tier-59m8</guid>
      <description>&lt;p&gt;There's a school of thought in creative fields — architecture, music, graphic design — that constraints produce better work than freedom. You don't write a sonnet because fourteen lines is the optimal poem length. You write a sonnet because the form forces decisions you wouldn't have made otherwise, and some of those decisions turn out to be the interesting ones.&lt;/p&gt;

&lt;p&gt;Software doesn't get talked about this way. Instead we talk about "optimization" and "cost reduction" like they're chores — things you do after the real design is done, or when someone notices the AWS bill needs its own line item in the board deck. We treat the budget as an obstacle to the architecture, something to apologize for, not something to design &lt;em&gt;with&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But the most elegant systems I've built weren't the ones where I had unlimited resources. They were the ones where the budget was effectively zero and the product still had to work. Start from that constraint — not as a failure mode but as a design input — and something different happens. You stop designing systems and start designing &lt;em&gt;within&lt;/em&gt; systems.&lt;/p&gt;

&lt;p&gt;This is a post about AWS's free tier. More specifically: how three years of building the cheapest possible production backends taught me that the constraints aren't obstacles to good architecture. They &lt;em&gt;are&lt;/em&gt; the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Free Tier Is Weirder and More Generous Than You Think
&lt;/h2&gt;

&lt;p&gt;Most people hear "AWS free tier" and think of the 12-month trial. Spin up an EC2 instance, forget about it, get a bill that makes you reconsider your career choices. That's not what I'm talking about.&lt;/p&gt;

&lt;p&gt;AWS has an &lt;em&gt;always-free&lt;/em&gt; tier. Not a trial. Not "free for your first year." Always free. And the serverless portion of it is genuinely, almost suspiciously generous.&lt;/p&gt;

&lt;p&gt;Here's what you get for nothing, forever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: 1 million invocations/month. 400,000 GB-seconds of compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt;: 1 million HTTP API calls/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt;: 25 GB of storage. 25 read capacity units, 25 write capacity units. (On-demand pricing has its own always-free allocation too.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: 5 GB storage. 20,000 GET requests. 2,000 PUT requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt;: 1 TB data transfer out. 10 million requests/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SES&lt;/strong&gt;: 3,000 messages/month if you're sending from Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognito&lt;/strong&gt;: 50,000 monthly active users. Fifty thousand. For free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that Cognito number again. Fifty thousand monthly active users on the auth layer. For a SaaS product. For free. If you have fifty thousand MAU, you either have revenue to pay for auth or you have much bigger problems than a Cognito bill.&lt;/p&gt;

&lt;p&gt;The combined effect of these isn't "you can run a toy project." It's "you can run a real SaaS product with real users and your infrastructure bill is, plausibly, zero." Not low. Zero.&lt;/p&gt;

&lt;p&gt;I spent three years at a company that specialized in exactly this. Lambda backends, DynamoDB data stores, the whole serverless stack. We weren't doing it for fun — we were doing it because the economics are absurd and our clients liked absurd economics. And somewhere along the way, the constraints stopped being constraints and started being a design language.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trap Doors
&lt;/h2&gt;

&lt;p&gt;Not everything in AWS is this benign. There are services that look like they belong in this stack and will absolutely ruin your zero-dollar streak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDS&lt;/strong&gt; is the big one. AWS's managed relational database service. It has a 12-month free tier — not always-free. After your first year, you're paying for a &lt;code&gt;db.t3.micro&lt;/code&gt; that costs roughly $15/month doing nothing. If you started a project on RDS because "I know SQL," congratulations: your note-taking app now has a recurring bill because you didn't want to learn DynamoDB's access pattern model. The constraint was trying to tell you something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2&lt;/strong&gt; is the other one. If you're running Lambda and you also have an EC2 instance "for the things Lambda can't do," you've left the free tier reservation. EC2's always-free allocation is 750 hours/month of &lt;code&gt;t2.micro&lt;/code&gt; — for 12 months. After that, it's metered. And if you're running something on EC2 that Lambda can't do, you should ask yourself whether you're building a serverless product or a server product that's embarrassed about it.&lt;/p&gt;

&lt;p&gt;The rule is simple: if the service doesn't have an always-free tier, it doesn't belong in the zero-dollar stack. Treat the 12-month trials as what they are — onboarding ramps that terminate in invoices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Build Something
&lt;/h2&gt;

&lt;p&gt;Let's experiment with this. Say you want to build a note-taking SaaS. Markdown-based, collaborative enough, the kind of thing a developer might actually use. Let's call it &lt;em&gt;Insight Notes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I have no notion as to why I'd name it that.&lt;/p&gt;

&lt;p&gt;Insight Notes needs: user authentication, a way to store and retrieve documents, a web frontend, transactional email (verification, password resets), and an API. That's it. That's most SaaS products, actually — a surprising number of them are just "auth + CRUD + a nice frontend" wearing different clothes.&lt;/p&gt;

&lt;p&gt;Here's the stack: Cognito for auth. Lambda for the API. API Gateway to route HTTP to Lambda. DynamoDB for document storage. S3 + CloudFront for the frontend. SES for email.&lt;/p&gt;

&lt;p&gt;Seven services. All of them have always-free tiers. All of them talk to each other natively without you writing glue code. And the total monthly cost for a product with, say, a few hundred active users?&lt;/p&gt;

&lt;p&gt;Somewhere between "rounding error" and "the price of a coffee." If you charge a dollar a month per user, you're keeping most of those hundred dollars. That's not a side project math trick — that's a margin most SaaS companies spend years of infrastructure work trying to approach.&lt;/p&gt;

&lt;p&gt;This is not a contrived example. This is basically every B2B SaaS with a different coat of paint — and if you can build &lt;em&gt;this&lt;/em&gt; cheaply, you can build most things cheaply.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Lambda to Rule Them All
&lt;/h2&gt;

&lt;p&gt;Here's the first thing three years of doing this professionally teaches you: use as few Lambdas as possible.&lt;/p&gt;

&lt;p&gt;The instinct, especially if you're coming from microservices, is to split things up. One Lambda for auth callbacks. One for document CRUD. One for search. One for email triggers. It's tidy. It's also wrong.&lt;/p&gt;

&lt;p&gt;Every Lambda has a cold start. The first invocation after a period of inactivity has to boot the runtime, load your code, initialize your connections. For a Node.js Lambda with reasonable dependencies, that's somewhere between 200ms and 800ms. For Java or .NET, multiply generously.&lt;/p&gt;

&lt;p&gt;One Lambda means one cold start. One user hitting your API with any kind of regularity keeps that Lambda warm. The website doesn't feel like it's booting up a server for every request — because it effectively isn't, as long as someone used it recently enough.&lt;/p&gt;

&lt;p&gt;Multiple Lambdas mean multiple independent cold starts. Your auth callback Lambda hasn't been invoked in an hour? Cold start. Your search Lambda? Also cold. Your user just waited 600ms for login and then another 500ms for their first search. The product feels broken and you haven't even done anything wrong — you just split your code the way the microservices blog told you to.&lt;/p&gt;

&lt;p&gt;One Lambda. Route internally. Use &lt;code&gt;lambda-api&lt;/code&gt; — a framework built specifically for this, with zero dependencies, designed around Lambda's execution model rather than retrofitted onto it. It handles the API Gateway proxy integration for you, parses requests, formats responses, and has a router that feels like Express without the thirty transitive dependencies Express brings to your cold start.&lt;/p&gt;

&lt;p&gt;Your single Lambda receives everything, routes it, and responds. Cold start happens once, warming benefits everything.&lt;/p&gt;

&lt;p&gt;This is the constraint producing the design. You're not choosing a monolith because monoliths are philosophically superior. You're choosing it because the cold start penalty makes the alternative feel terrible to use. The constraint said "you get one warm execution environment" and the design fell out of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  DynamoDB: The 25 GB Puzzle
&lt;/h2&gt;

&lt;p&gt;DynamoDB is the most opinionated database you will ever use and it's free for 25 GB. Whether that's a gift or a curse depends entirely on whether you're willing to think about your data the way DynamoDB wants you to.&lt;/p&gt;

&lt;p&gt;If you're coming from Postgres or MySQL, your first instinct will be to model your data relationally. You'll want foreign keys, you'll want JOINs, you'll want to normalize everything into tidy third-normal-form tables.&lt;/p&gt;

&lt;p&gt;Your second instinct will be to search for the AWS serverless SQL solution. That's the devil speaking. Aurora Serverless exists, and it will let you write &lt;code&gt;SELECT * FROM notes WHERE user_id = ?&lt;/code&gt; like a civilized person, and it will also cold start for up to 30 seconds on the first connection, bill you per ACU-hour whether you're doing anything or not, and cheerfully generate a surprise invoice the moment you get any real traffic. It is not a free tier play. It is not even a cheap play. It is a trap with a familiar interface.&lt;/p&gt;

&lt;p&gt;So: DynamoDB. And look — the instinct to resist it is correct, because DynamoDB is genuinely strange. But strange in a way that pays off.&lt;/p&gt;

&lt;p&gt;DynamoDB will let you design a relational model. DynamoDB will then punish you for it at read time, slowly, expensively, and without remorse. What it wants instead is single-table design. It wants you to think about your access patterns &lt;em&gt;first&lt;/em&gt; and your data model &lt;em&gt;second&lt;/em&gt;. How will Insight Notes be queried? By user ID. By document ID. By user ID sorted by last modified. That's three access patterns, and if you're clever about your partition key and sort key, that's one table.&lt;/p&gt;

&lt;p&gt;For Insight Notes, that table might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;SK&lt;/th&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#anna&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROFILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ email, name, created }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#anna&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NOTE#01JADX...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ title, content, updated }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#anna&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FOLDER#work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ name, color, created }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER#anna&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NOTE#01JADX...#META&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ folder: "work", tags: [...] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The raw key structure is what DynamoDB actually stores. What you write in code looks considerably more civilized — &lt;code&gt;dynaorm&lt;/code&gt; is a type-safe client that handles marshaling and unmarshaling, validates your items against a Zod schema before they touch the database, and gives you a fluent query builder so &lt;code&gt;.query().wherePK("USER#anna").whereSK("begins_with", "NOTE#")&lt;/code&gt; does exactly what it looks like. The constraint forced the data model. The library makes the data model livable.&lt;/p&gt;

&lt;p&gt;Everything for one user lives under one partition key. Getting all of a user's notes is a single query. Getting a specific note is a point read. Getting all notes in a folder requires a secondary index or a filter — and this is where the design constraint forces you to think about access patterns &lt;em&gt;before&lt;/em&gt; you write a single line of code. In Postgres, you'd add a &lt;code&gt;WHERE folder_id = ?&lt;/code&gt; and not think about it. In DynamoDB, that query either needs to be modeled into the table structure or it costs you an index. Which means you design around how you read, not how you write. Which — and this is the art part — often produces a better data model than the relational one, because you're forced to think about actual user experience instead of abstract data relationships.&lt;/p&gt;

&lt;p&gt;The 25 GB free tier sounds small until you do the math. A markdown document is text. Text is small. At 3 KB average per note — title, content, metadata, organizational data — 25 GB holds roughly 8 million notes. That's not a toy constraint. That's a constraint most products will never actually reach.&lt;/p&gt;

&lt;p&gt;The real constraint is throughput, not storage. 25 read capacity units and 25 write capacity units is roughly 25 strongly-consistent reads per second for items up to 4 KB, and 25 writes per second for items up to 1 KB. For Insight Notes with a few hundred users, that's fine. For thousands of concurrent users all editing simultaneously — you'll need on-demand capacity, which still has a free allocation but works differently.&lt;/p&gt;

&lt;p&gt;The point is: the limit makes you think. Think about item size. Think about access patterns. Think about what "enough" means for your actual product, not some hypothetical scale you haven't earned yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Frontend: Smaller Is Literally Cheaper
&lt;/h2&gt;

&lt;p&gt;S3 + CloudFront for static hosting is standard. What's less obvious is that the free tier makes your frontend size a &lt;em&gt;financial&lt;/em&gt; concern, not just a performance one.&lt;/p&gt;

&lt;p&gt;5 GB of S3 storage is plenty for a frontend. But 20,000 GET requests per month means every asset your page loads counts against a real number. And while CloudFront's 10 million requests and 1 TB transfer are generous, the S3 origin requests behind it aren't free once you exceed the allocation.&lt;/p&gt;

&lt;p&gt;So your React bundle size isn't just a Lighthouse score. It's a line item. Fewer assets, smaller bundles, aggressive caching headers — these aren't best practices you should get around to someday. They're the difference between a zero-dollar bill and a not-zero-dollar bill.&lt;/p&gt;

&lt;p&gt;This is where the constraint does its best work. You were &lt;em&gt;supposed&lt;/em&gt; to ship a smaller frontend. You were &lt;em&gt;supposed&lt;/em&gt; to set proper cache headers. You were &lt;em&gt;supposed&lt;/em&gt; to lazy-load that charting library nobody uses on the landing page. The free tier just gave you a reason that shows up on an invoice instead of a performance audit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part Where Autoscaling Tries to Bankrupt You
&lt;/h2&gt;

&lt;p&gt;Here's a thing about serverless that nobody warns you about until it's too late.&lt;/p&gt;

&lt;p&gt;Traditional servers crash under load. That's bad, but it's also a natural circuit breaker. Your server falls over, users get errors, you wake up and fix it. There's a ceiling.&lt;/p&gt;

&lt;p&gt;Lambda doesn't crash under load. Lambda scales. Automatically. To whatever your concurrency limit allows. A thousand concurrent invocations? Lambda will handle it. Ten thousand? Sure, if your account limit permits. DynamoDB on-demand? Scales to meet the request rate. API Gateway? Routes it all through.&lt;/p&gt;

&lt;p&gt;This is excellent until someone writes a bot that hits your API ten million times in a day. Or until a legitimate usage spike pushes you past the free tier allocation on every service simultaneously. Or until a misconfigured retry loop in your own frontend hammers your own backend at scale.&lt;/p&gt;

&lt;p&gt;The traditional server would have crashed. Your bill would have been zero because the server was down. The serverless stack stays up. The serverless stack &lt;em&gt;scales to meet the demand&lt;/em&gt;. The serverless stack sends you a bill for meeting that demand.&lt;/p&gt;

&lt;p&gt;You need rate limiting. You need it at the API Gateway level (throttling is built in, configure it). You need it at the application level (per-user, per-endpoint, per-operation). You need CloudFront caching in front of everything that can be cached, not for performance but for cost containment. You need billing alerts — AWS lets you set them, and you should set them at thresholds that make you uncomfortable, like $1 and $5 and $10, because the jump from $0 to $50 happens fast when the constraint you relied on was "nobody's using this yet."&lt;/p&gt;

&lt;p&gt;Caching, rate limiting, and billing alerts aren't operational maturity for a zero-dollar product. They're structural requirements. The system doesn't crash anymore. Which means the system doesn't stop you from spending money anymore. That's your job now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deal With the Devil
&lt;/h2&gt;

&lt;p&gt;I've spent this entire post talking about AWS services like they're building blocks in a design exercise. And they are. But I need to be honest about what you're actually doing when you build this way, because the constraint-as-art metaphor has a dark edge.&lt;/p&gt;

&lt;p&gt;This stack — Lambda, API Gateway, DynamoDB, S3, CloudFront, SES, Cognito — is not "cloud-native." It's &lt;em&gt;AWS-native&lt;/em&gt;. There's a difference, and the difference matters.&lt;/p&gt;

&lt;p&gt;DynamoDB is not Postgres. Your single-table design, your GSIs, your DynamoDB Streams triggers — none of that transfers to Azure or GCP or your own hardware. Lambda's execution model, its cold start characteristics, its integration with API Gateway — those are AWS implementation details dressed up as abstractions. Cognito's user pools, its hosted UI, its token format — AWS-specific.&lt;/p&gt;

&lt;p&gt;If you build Insight Notes on this stack and then decide to move to a different cloud provider, you are rewriting most of your backend. Not migrating. Rewriting. The data model changes because DynamoDB's model is DynamoDB's. The auth layer changes because Cognito is Cognito. The compute model changes because Lambda is Lambda.&lt;/p&gt;

&lt;p&gt;This is the deal. AWS gives you an extraordinarily generous free tier on services that are extraordinarily specific to AWS. The generosity and the lock-in are the same feature. They want you to build something real on their platform, for free, because they know that "for free" becomes "too expensive to move" once your product has users and your data model is shaped like DynamoDB.&lt;/p&gt;

&lt;p&gt;And here's the part that makes the deal complicated rather than simple: for a lot of products, this is fine. Insight Notes doesn't need multi-cloud portability. Most SaaS products don't. The exit cost is real, but the exit is hypothetical, and the operational cost of &lt;em&gt;not&lt;/em&gt; using the purpose-built services — of running your own Postgres on EC2, your own auth on a container, your own email infrastructure — is higher than the lock-in cost for any product that isn't planning to leave AWS.&lt;/p&gt;

&lt;p&gt;The constraint is: you're building on AWS's terms. The art is knowing that, choosing it deliberately, and designing within those terms rather than pretending they don't exist. Don't use DynamoDB and then complain that it's not Postgres. Don't use Lambda and then complain about cold starts. You chose these. They chose you back.&lt;/p&gt;




&lt;h2&gt;
  
  
  $200 and a Plan
&lt;/h2&gt;

&lt;p&gt;AWS gives you $200 in credits when you sign up. Combined with the always-free tier, that's not a toy budget. That's "launch a product, get your first paying customers, and let the revenue catch up to the infrastructure" budget.&lt;/p&gt;

&lt;p&gt;The credits cover the things the free tier doesn't — the occasional Lambda burst that exceeds 1M invocations, the DynamoDB spike during a product launch, the SES costs once you're sending more than 3,000 emails/month. Think of the credits as the buffer between "this is free" and "this costs money but the money is mine now."&lt;/p&gt;

&lt;p&gt;Most products that fail don't fail because of infrastructure costs. They fail because nobody used them. The serverless free tier means infrastructure costs are the last thing that kills you, which means you get to fail for the right reasons instead.&lt;/p&gt;




&lt;p&gt;There's something deeply satisfying about building a system where every design decision has a reason and half those reasons are "because the free tier works this way." It's like writing a sonnet, except the meter is measured in Lambda invocations and the rhyme scheme is your DynamoDB access patterns.&lt;/p&gt;

&lt;p&gt;The constraints are real. The lock-in is real. The risk of accidentally autoscaling yourself a bill is real. But the product is also real, and it cost you nothing to build, and the decisions the constraints forced on you — one Lambda, single-table design, tiny frontend, aggressive caching — are decisions you should have been making anyway.&lt;/p&gt;

&lt;p&gt;Design constraints as art. AWS as the medium. Your invoice as the critic.&lt;/p&gt;

&lt;p&gt;Just set up rate limiting first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@thisisengineering" rel="noopener noreferrer"&gt;© This is Engineering&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Infisical is Great, Actually</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Fri, 27 Mar 2026 20:00:00 +0000</pubDate>
      <link>https://dev.to/notjustanna/infisical-is-great-actually-13ho</link>
      <guid>https://dev.to/notjustanna/infisical-is-great-actually-13ho</guid>
      <description>&lt;p&gt;I run ArgoCD. Full GitOps — if it's not in the repo, it doesn't exist. That's great for everything except secrets, where "if it's in the repo, it might not exist for long either." GitHub secret scanning will catch an API key in a private repo, helpfully disable it, and send you a polite notification that you messed up. So I needed an ESO backend. Here's what I looked at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopping Around
&lt;/h2&gt;

&lt;p&gt;I was already applying secrets manually via &lt;code&gt;kubectl&lt;/code&gt; — which works fine until it doesn't, and doesn't scale past "just me doing everything." The plan was always to wire up External Secrets Operator; the question was just what it would point at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SOPS&lt;/strong&gt; came up first — a Claude recommendation. It encrypts secrets in-repo, which sounds elegant, but the decryption key has to live somewhere, and in practice that somewhere is the machine doing the decrypting. If that machine is compromised, the attacker gets the key, and the key opens everything. Security theater. My brain wanted something that felt like AWS Parameter Store — a place secrets live, accessed over an authenticated API, not a place they're hidden inside something else with the unhiding tool sitting right next to them.&lt;/p&gt;

&lt;p&gt;The second AI-generated recommendation was &lt;strong&gt;GitHub Actions Secrets&lt;/strong&gt;. Which, sure — except my IaC repo &lt;em&gt;has&lt;/em&gt; GitHub Actions in it. Secrets that live inside the same system they're deploying feel like a liability waiting to happen. At this point it was clear I needed an actual secrets service, so I started shopping for an ESO backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Secrets Manager&lt;/strong&gt;, &lt;strong&gt;AWS Parameter Store&lt;/strong&gt;, and &lt;strong&gt;OCI Vault&lt;/strong&gt; were the clear, industry-standard options — the real products that do the job properly. But I'm running K3s on a free OCI ARM VM specifically because I want zero dependencies I can't walk away from. If OCI's free tier ever goes south, I want to pack up and leave — depending on OCI Vault would chain my secrets to the same cloud I'm trying to stay portable from. And pulling in AWS just for secrets would be adding a second cloud dependency for no reason.&lt;/p&gt;

&lt;p&gt;I also stumbled upon &lt;strong&gt;Doppler&lt;/strong&gt;, which I actually liked the look of. The DX is genuinely good, the CLI is pleasant, the UI is clean. Then I hit the pricing page: service accounts — the thing you need for any automated workflow — are a Team plan feature. $21/month per user. For just me. For secrets. No.&lt;/p&gt;

&lt;p&gt;Then I found Infisical.&lt;/p&gt;

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

&lt;p&gt;Infisical is an open-source secrets manager with a managed cloud offering and a self-hostable option. The cloud tier is genuinely free for reasonable usage. It has a UI, a CLI, SDKs, and first-class Kubernetes support — either via its own operator or as an External Secrets Operator backend, which is what I ended up using.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup I Actually Use: ESO + Infisical Cloud
&lt;/h2&gt;

&lt;p&gt;Rather than the Infisical operator, I went with &lt;a href="https://external-secrets.io/" rel="noopener noreferrer"&gt;External Secrets Operator&lt;/a&gt; (ESO) using Infisical as the backend. ESO is a CNCF project with a clean abstraction: you define a &lt;code&gt;SecretStore&lt;/code&gt; (or &lt;code&gt;ClusterSecretStore&lt;/code&gt;) pointing at your secrets backend, then &lt;code&gt;ExternalSecret&lt;/code&gt; resources that describe which secrets to sync and where. The output is always a standard Kubernetes &lt;code&gt;Secret&lt;/code&gt;. Swap out the backend someday and your app manifests don't change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing ESO via ArgoCD
&lt;/h3&gt;

&lt;p&gt;I manage everything with ArgoCD, so the ESO install is an Application pointing at the official release manifest:&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="c1"&gt;# applications/external-secrets/kustomization.yml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://github.com/external-secrets/external-secrets/releases/download/v2.2.0/external-secrets.yaml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cluster-secret-stores.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ArgoCD Application itself:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&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;argocd.argoproj.io/sync-wave&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/notjustanna/iac.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;applications/external-secrets&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ServerSideApply=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sync-wave: "-1"&lt;/code&gt; ensures ESO is fully deployed before anything tries to create &lt;code&gt;ExternalSecret&lt;/code&gt; resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the ClusterSecretStore
&lt;/h3&gt;

&lt;p&gt;Create a machine identity in Infisical (Project → Access Control → Machine Identities), give it read access to your environment, then store the credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic infisical-auth &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-client-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;clientSecret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-client-secret&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; external-secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;code&gt;ClusterSecretStore&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="c1"&gt;# applications/external-secrets/cluster-secret-stores.yml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterSecretStore&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infisical&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;infisical&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;universalAuthCredentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infisical-auth&lt;/span&gt;
            &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clientId&lt;/span&gt;
          &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infisical-auth&lt;/span&gt;
            &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clientSecret&lt;/span&gt;
      &lt;span class="na"&gt;secretsScope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;projectSlug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-project-slug"&lt;/span&gt;
        &lt;span class="na"&gt;environmentSlug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod"&lt;/span&gt;
        &lt;span class="na"&gt;secretsPath&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="na"&gt;recursive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;ClusterSecretStore&lt;/code&gt; serves the whole cluster. Every namespace can reference it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consuming Secrets: A Real Example
&lt;/h3&gt;

&lt;p&gt;Here's how I pull in the Cloudflare API token for Traefik. In Infisical, the secret lives under &lt;code&gt;/traefik&lt;/code&gt;. The &lt;code&gt;ExternalSecret&lt;/code&gt; syncs everything under that path into a Kubernetes secret in the &lt;code&gt;traefik&lt;/code&gt; namespace:&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="c1"&gt;# applications/traefik/external-secret.yml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExternalSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare-api-token&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;secretStoreRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infisical&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterSecretStore&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare-api-token&lt;/span&gt;
    &lt;span class="na"&gt;creationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Owner&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
  &lt;span class="na"&gt;dataFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/traefik&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik just sees a Kubernetes secret. It has no idea Infisical exists. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Actually Sold Me
&lt;/h2&gt;

&lt;p&gt;The ESO pattern means &lt;strong&gt;application manifests don't change&lt;/strong&gt; when you change secret backends. Secrets are just Kubernetes secrets to everything downstream — no SDK, no sidecar, no secret-fetching logic in application code. Infisical lives entirely in the infra layer, which is where secrets management should live.&lt;/p&gt;

&lt;p&gt;The path-based organization (&lt;code&gt;/traefik&lt;/code&gt;, &lt;code&gt;/monitoring&lt;/code&gt;, and so on) felt immediately familiar — it's the same mental model as AWS Parameter Store. That's not a coincidence; it's just the right way to organize secrets. The &lt;code&gt;recursive: true&lt;/code&gt; on the &lt;code&gt;ClusterSecretStore&lt;/code&gt; means &lt;code&gt;ExternalSecret&lt;/code&gt; resources can scope as narrowly or broadly as makes sense per workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Use the Cloud or Self-Host?
&lt;/h2&gt;

&lt;p&gt;The managed free tier covers up to 5 projects with unlimited secrets. For a homelab or small production workload, there's genuinely no reason to self-host unless you want to.&lt;/p&gt;

&lt;p&gt;If you do want to self-host — the Helm chart is well-documented. Just be aware of the chicken-and-egg problem: if Infisical lives on the same cluster it's serving secrets to, it becomes a bootstrap dependency, and that gets messy fast. I wrote about this failure mode &lt;a href="https://dev.tolink-to-chicken-and-egg-post"&gt;in more detail here&lt;/a&gt;. Infisical Cloud sidesteps it entirely, which is why I'm using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;I stopped looking for a better option. Infisical + ESO hit the threshold of "this is clearly correct" — open source, free at the scale I need, Kubernetes-native without being invasive, and not locked to any cloud. The setup I showed above is the whole thing. If you're still managing secrets via committed files, manual &lt;code&gt;kubectl create secret&lt;/code&gt; commands, or CI-only stores — this is the way out.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@sidverma" rel="noopener noreferrer"&gt;Sid Verma&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>kubernetes</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Self-Hosting Everything, Including the Single Point of Failure</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Fri, 27 Mar 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/notjustanna/self-hosting-everything-including-the-single-point-of-failure-28dm</link>
      <guid>https://dev.to/notjustanna/self-hosting-everything-including-the-single-point-of-failure-28dm</guid>
      <description>&lt;p&gt;Homelabbing is genuinely fun. I want to say that upfront, before I tell you about the time I locked myself out of my own infrastructure for an afternoon.&lt;/p&gt;

&lt;p&gt;The premise is compelling: you have a VM, you have K3s, and the open source ecosystem has basically everything you'd pay a SaaS for. Keycloak for OIDC. Forgejo for Git. Headscale for VPN. ArgoCD for GitOps. A few YAML files and you have a self-hosted stack that would make a startup founder weep.&lt;/p&gt;

&lt;p&gt;And for a while, it works beautifully. Ansible (yes, Ansible — I've since retired it, but that's &lt;a href="https://dev.to/notjustanna/containers-the-wrong-way-lessons-learnt-4obn"&gt;another post&lt;/a&gt;) kept everything converging to the right state. ArgoCD synced my apps from a Forgejo repo. Kubectl reached K3s over Headscale. Users — well, me — logged into Forgejo via Keycloak OIDC. My custom Keycloak theme was hosted on Forgejo.&lt;/p&gt;

&lt;p&gt;It was elegant. It was self-referential. It was fine, right up until it wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dependency Graph Nobody Warned Me About
&lt;/h2&gt;

&lt;p&gt;Let me draw the graph for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD&lt;/strong&gt; deploys everything, including Forgejo, Keycloak and Headscale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgejo&lt;/strong&gt; hosts the GitOps repo ArgoCD syncs from&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keycloak&lt;/strong&gt; handles auth for Forgejo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgejo&lt;/strong&gt; hosts my custom Keycloak theme&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headscale&lt;/strong&gt; is how my &lt;code&gt;kubectl&lt;/code&gt; reached K3s remotely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You see the problem. The dependency graph doesn't have a root. Everything depends on something else that it manages. It's turtles all the way down, except the turtles are also responsible for each other's wellbeing.&lt;/p&gt;

&lt;p&gt;This is fine when everything is running. A self-healing cycle. ArgoCD keeps things in sync, everything stays up, life is good.&lt;/p&gt;

&lt;p&gt;It is considerably less fine when Oracle decides to release a new Ubuntu Minimal image and &lt;code&gt;terraform apply&lt;/code&gt; silently replaces your VM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do Not &lt;code&gt;terraform apply&lt;/code&gt; While Distracted
&lt;/h2&gt;

&lt;p&gt;I won't go into the specifics of how Oracle releasing a new image source caused my Terraform to replace the VM — that's a configuration lesson for another day, and also too embarrassing to fully recount. The point is: one moment I had a running K3s node, the next I had a fresh VM.&lt;/p&gt;

&lt;p&gt;And everything on it was gone.&lt;/p&gt;

&lt;p&gt;This was before the containers setup. No clean &lt;code&gt;/data&lt;/code&gt; mount, no steward, no "two and a half minutes to a working kubernetes." Just a blank VM and the knowledge that everything I needed to recover was hosted on the thing that no longer existed.&lt;/p&gt;

&lt;p&gt;And separately: Headscale was down because ArgoCD hadn't deployed it yet — and Headscale was specifically what kubectl used to reach K3s remotely. No block volume, no &lt;code&gt;/data&lt;/code&gt;, no steward. Cloud-init got as far as reloading K3s and standing up a blank ArgoCD — the Ansible playbook was in an OCI bucket, so at least that survived — but with no way to kubectl in, that was as far as it got.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chicken, meet egg.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was ugly: I had to edit the Ansible playbook to point ArgoCD at GitHub instead of Forgejo, push that, let it redeploy — and then begin a long series of commits that were mostly commenting and uncommenting service configs. Each change exposed a new chicken-and-egg problem. Forgejo needs Keycloak. Keycloak needs Forgejo for the theme. ArgoCD needs Forgejo. Comment out the OIDC config, deploy, uncomment, redeploy, something else falls over. Repeat.&lt;/p&gt;

&lt;p&gt;When it was finally stable, I left myself a note: &lt;em&gt;disassemble this before it explodes again.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Self-referential systems need an escape hatch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern I had — GitOps repo on the same machine GitOps is managing — is a known antipattern. The right answer is that your bootstrap source should be independent of what you're bootstrapping.&lt;/p&gt;

&lt;p&gt;But here's the thing: I &lt;em&gt;knew&lt;/em&gt; what I was building had philosophical coherence. The whole point was to depend less on SaaS. Not just for the homelab, but as a stance — I wanted off GitHub, off Tailscale, off the assumption that someone else's free tier was load-bearing infrastructure. Running Forgejo on the same cluster wasn't carelessness; it was the logical extension of that goal. Own everything, all the way down.&lt;/p&gt;

&lt;p&gt;What I hadn't stress-tested was the dependency graph when the cluster itself was the patient.&lt;/p&gt;

&lt;p&gt;The mistake wasn't Forgejo — it was Forgejo &lt;em&gt;on the same cluster it was managing&lt;/em&gt;. Those are different decisions. And honestly, the right fix for wanting off GitHub isn't to run Forgejo on your only VM; it's to use something like Codeberg. You can move off SaaS without taking on the operational risk of a self-hosted bootstrap source. The self-hosting purist move and the pragmatic resilience move don't have to be the same decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headscale was behind the locked door.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason the chicken-and-egg problem was so painful wasn't just the circular deployments — it's that Headscale was my only path to &lt;code&gt;kubectl&lt;/code&gt;. When the cluster died, my remote access died with it. The tool I needed to fix my infrastructure was running on the infrastructure I needed to fix.&lt;/p&gt;

&lt;p&gt;This is the same lesson as Forgejo, applied to access instead of deployment: if something is part of your recovery path, it can't live inside the thing you're recovering.&lt;/p&gt;

&lt;p&gt;I genuinely enjoyed running Headscale — the project is impressive and there's something satisfying about owning your own VPN coordination. But after the incident, I couldn't justify self-hosting the thing that stands between me and my cluster. Tailscale's free tier covers 100 devices and 3 users. I am one person with a homelab. I migrated back and haven't thought about it since.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some self-hosting is load-bearing in ways that compound.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After all this, I dropped Forgejo and Keycloak too — at least for now. The overhead of maintaining the auth layer, the themes, the OIDC integration, stopped being worth it relative to what I was actually getting. The homelab is lighter for it.&lt;/p&gt;

&lt;p&gt;What I kept: the lesson. If something is load-bearing for your bootstrap sequence, it needs to be independent of what it's bootstrapping. That's the rule. Everything else is negotiable.&lt;/p&gt;




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

&lt;p&gt;I self-hosted Headscale. I learned what Tailscale actually provides, operationally, by having to do all of it myself. I made an informed decision to go back.&lt;/p&gt;

&lt;p&gt;I self-hosted Forgejo inside a circular dependency and paid for it when the cluster went down. I moved the bootstrap repo off the critical path — and eventually off the cluster entirely.&lt;/p&gt;

&lt;p&gt;The homelab taught me which abstractions were worth paying for (VPN coordination — yes, for me, right now). It also taught me that the interesting self-hosting problems aren't "can I run this" — you always can — but "where does this fit in the dependency graph, and what happens when it's down."&lt;/p&gt;

&lt;p&gt;Homelabbing is fun. Highly recommend. Just draw the dependency graph first.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@eternalseconds" rel="noopener noreferrer"&gt;Eastman Childs&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>homelab</category>
      <category>gitops</category>
      <category>postmortem</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>I Run Nomad on my Gaming PC (It's Great)</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Thu, 26 Mar 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/notjustanna/i-run-nomad-on-my-gaming-pc-its-great-e5f</link>
      <guid>https://dev.to/notjustanna/i-run-nomad-on-my-gaming-pc-its-great-e5f</guid>
      <description>&lt;p&gt;HashiCorp Nomad is a workload orchestrator. Think Kubernetes, but without the container-first dogma — it can schedule containers, sure, but also raw executables, Java applications, scripts, whatever you have. It's designed for fleets: multiple datacenters, hundreds of nodes, cross-machine scheduling. The kind of infrastructure where "where does this service run" is a question Nomad answers for you.&lt;/p&gt;

&lt;p&gt;That's the intended use case. Here's another one.&lt;/p&gt;




&lt;p&gt;My first professional encounter with Nomad was at a company where our team had no RDP access to the Windows Server we were deploying to. No remote desktop, no SSH, nothing — just Nomad and whatever we chose to run through it.&lt;/p&gt;

&lt;p&gt;So Nomad became everything. Restart a service? Nomad. Check backend logs? Nomad. Copy files onto the machine? We deployed Filebrowser through Nomad so we could do that too. The machine was, for all practical purposes, a black box we could only interact with through the web UI and job specs.&lt;/p&gt;

&lt;p&gt;This wasn't one rogue team. There were a dozen teams doing the same thing, each with their own trio of Windows Servers — DEV, STG, PRD. Each machine a proper behemoth: 32 or 64GB of RAM, terabytes of storage, quietly running 20-something JVM-based services that would eventually consume most of that memory. Nomad scheduling all of it. Single node, pointed at itself.&lt;/p&gt;

&lt;p&gt;Was this production? Yes. Was it highly available? No — it was roughly on par with a Linux box running systemd services, just with a shinier interface and HCL files instead of unit files. But it ran. Teams shipped things. Nomad, against all architectural intent, worked.&lt;/p&gt;

&lt;p&gt;None of us questioned this. I had come from SSH, Docker Compose, Portainer — tools where "the interface to the machine" and "the service manager" are different things. Nomad fit neatly into my mental model as &lt;em&gt;Portainer but for bare-metal Windows&lt;/em&gt;. Web UI, log access, restart buttons. Close enough.&lt;/p&gt;

&lt;p&gt;I only found out this was off-label when I finally read the HashiCorp docs. They were very clearly written for someone orchestrating fleets. Multi-datacenter topology. Node pools. Cross-machine scheduling. I was running one agent, pointed at itself, on a machine I couldn't even open a terminal on. The docs and I were not describing the same situation.&lt;/p&gt;

&lt;p&gt;And yet: it worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it followed me home
&lt;/h2&gt;

&lt;p&gt;When I decided to self-host Jellyfin and DDNS-Go on my personal machine — which, at the time, ran Windows — I reached for the tool I knew.&lt;/p&gt;

&lt;p&gt;Windows, as a self-hosting platform, has two notable properties: it has no good log story, and it has no good service management UI. Task Scheduler exists. Services exist. Neither of them will make you feel good about yourself.&lt;/p&gt;

&lt;p&gt;Nomad had a web UI I could reach from anywhere on my Tailscale network, live log tailing, and a restart button. I wrote a job spec, set up &lt;code&gt;raw_exec&lt;/code&gt;, and Jellyfin was running. Logs accessible. Restartable from work, from my phone, from wherever — no terminal required.&lt;/p&gt;




&lt;h2&gt;
  
  
  And then I migrated to Linux
&lt;/h2&gt;

&lt;p&gt;Nomad came with me.&lt;/p&gt;

&lt;p&gt;This is, I think, the real testament to the setup. When I finally moved to Linux — where proper service management exists, where &lt;code&gt;journalctl&lt;/code&gt; is right there, where I had every excuse to do it correctly — I kept Nomad. Because clicking through a web UI beats memorizing &lt;code&gt;journalctl&lt;/code&gt; flags. Because I already had the job specs written and they just worked.&lt;/p&gt;

&lt;p&gt;My gaming PC now runs a Nomad agent pointed at itself. A cluster of one. Same off-label usage I accidentally learned at work, just with less mystery Windows Server and significantly more RGB.&lt;/p&gt;

&lt;p&gt;When I moved to Linux, I didn't just migrate Jellyfin and DDNS-Go. I added Sunshine to the roster too. And Code Tunnel. The thing I originally wanted to restart from work — the whole reason I went down this path — ended up as just another Nomad job. One more entry in the web UI. Restartable from anywhere, as long as I have a VPN connection to the PC.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is this wrong?
&lt;/h2&gt;

&lt;p&gt;Technically, yes. Nomad is built for fleets. Running it on one node is like using Kubernetes to manage your dotfiles.&lt;/p&gt;

&lt;p&gt;But the off-label usage holds up — and I say this as someone who stumbled into it by accident and only realized it was off-label afterward. It survived a Windows Server I had no other way to access. It survived being someone's actual production infrastructure across an entire organization. It survived my personal Windows machine. It survived a migration to Linux where I had every incentive to switch. What you get at the end of all that is a web UI where you can see your services, read their logs, and restart them from anywhere — and a declarative spec file that travels with you across operating systems.&lt;/p&gt;

&lt;p&gt;At some point you stop calling it wrong and start calling it yours.&lt;/p&gt;

&lt;p&gt;Sometimes the right tool is the one you already know how to use. Even if you learned it wrong.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@ba1kouras" rel="noopener noreferrer"&gt;Balkouras Nicos&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>nomad</category>
      <category>devops</category>
    </item>
    <item>
      <title>Containers, The Wrong Way: Lessons Learnt</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Wed, 25 Mar 2026 23:41:50 +0000</pubDate>
      <link>https://dev.to/notjustanna/containers-the-wrong-way-lessons-learnt-4obn</link>
      <guid>https://dev.to/notjustanna/containers-the-wrong-way-lessons-learnt-4obn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is a follow-up of &lt;a href="https://dev.to/notjustanna/containers-the-wrong-way-for-always-free-fun-and-profit-3ea1"&gt;"Containers, The Wrong Way, For Always-Free Fun and Profit"&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my last post, I told you all a wild idea: stop caring about the host OS of your EC2/VM. Take the OS hostage. Make it a babysitter of privileged container, and from that point on it's as relevant as a bastion VM. Your environment lives in an Docker/Podman image.&lt;br&gt;
Versioned, reproducible, and testable on your laptop/QEMU/VMWare.&lt;/p&gt;

&lt;p&gt;A week later, &lt;code&gt;119 files changed, +612 -4210 lines changed&lt;/code&gt; (this is what an Ansible retirement looks like) and I have one thing to say:&lt;/p&gt;

&lt;p&gt;The core idea was right. I just hadn't "thought with containers" all the way through.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prelude: The host OS matters. A tiny bit.
&lt;/h2&gt;

&lt;p&gt;Here's the thing about the "host OS doesn't matter" premise: it only holds if the host OS &lt;em&gt;agrees to not matter&lt;/em&gt;. Your privileged container needs to start and be able to take the host OS hostage. That's the whole deal. The host gets you to that point, and then it gets out of the way.&lt;/p&gt;

&lt;p&gt;Oracle Linux ships with SELinux enforcing by default. And SELinux, doing exactly what SELinux is designed to do, looked at my privileged container with host networking and nested containers and said: wait just a goshdarned second. And, to be honest? &lt;em&gt;SELinux is right&lt;/em&gt;. Windows Defender would have a field day trying to defend itself against this OS-level hijacking attack with admin rights.&lt;/p&gt;

&lt;p&gt;The fix is... straightforward enough — set SELinux to permissive, reboot. But that's the problem for me. This would mean writing a whole "is SELinux enabled? try disable it and reboot" script to my cloud-init, which is already way too big IMHO.&lt;/p&gt;

&lt;p&gt;This isn't Oracle Linux being bad. It isn't SELinux being wrong. It's a contract violation: I needed a host that would get out of the way, and this one wouldn't.&lt;/p&gt;

&lt;p&gt;So... back to Ubuntu 24.04 Minimal. No drama. &lt;code&gt;unattended-upgrades&lt;/code&gt; at 6am UTC. The host goes back to being furniture.&lt;/p&gt;


&lt;h2&gt;
  
  
  I Wasn't Actually Thinking With Containers
&lt;/h2&gt;

&lt;p&gt;My original idea was, conceptually: take whatever free Linux distro the cloud handed you, bolt a privileged container with Alpine, run everything inside. One container. One image. K3s, Tailscale, manifests, startup logic — all of it, together.&lt;/p&gt;

&lt;p&gt;I thought I was thinking with containers. I was actually thinking &lt;em&gt;"how do I run a VM without virtualizing another Linux kernel."&lt;/em&gt; The question I should have asked earlier: &lt;em&gt;why stop at one container?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rancher/k3s&lt;/code&gt; is a scratch image. No shell, no package manager, nothing. It ships that way intentionally — K3s bundles exactly what it needs and nothing else. The moment I tried to extend it, I was working against a clear signal. The image was telling me something: &lt;em&gt;don't touch me, use me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same with Tailscale. &lt;code&gt;tailscale/tailscale&lt;/code&gt; exists, maintained by the people who wrote Tailscale, optimized for exactly this use case. Why was I installing &lt;code&gt;tailscaled&lt;/code&gt; inside my Alpine image?&lt;/p&gt;

&lt;p&gt;Instead of fighting the host OS, I was now fighting my own container image. All the pieces existed upstream, and yet, I was trying to further disassemble them.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Steward Container
&lt;/h2&gt;

&lt;p&gt;I once used Portainer to manage an entire fleet of VMs and baremetal servers. Got a lot of flack from the internet for using it, too. Portainer manages your container, but it too is a container. It just required mounting the Docker socket, and it managed everything. I should have done that from the beginning.&lt;/p&gt;

&lt;p&gt;My once massive container image quickly shrunk into a &lt;strong&gt;steward container&lt;/strong&gt; — a thin Alpine image whose only job is orchestrating other containers. It doesn't run K3s. It doesn't run Tailscale. It uses &lt;code&gt;podman-compose&lt;/code&gt; to bring them up and manages their lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:3.21&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; bash podman podman-compose gettext kubectl curl

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /image&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["sh", "-c"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/image/steward.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;K3s runs from &lt;code&gt;rancher/k3s:v1.35.2-k3s1&lt;/code&gt;. Tailscale runs from &lt;code&gt;tailscale/tailscale:v1.94.2&lt;/code&gt;. Both are purpose-built, upstream, and updated by bumping a version tag. I don't own their&lt;br&gt;
internals. I just sequence them.&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/tailscale/tailscale:v1.94.2&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="na"&gt;k3s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/rancher/k3s:v1.35.2-k3s1&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And because the steward is just glue — Alpine, bash, Podman Compose — it's entirely mine to change. If I want to rewire how bootstrap works, add a new sequencing step, or swap out how secrets are injected, I edit the steward. K3s and Tailscale don't care — they just get started in a different order, or with different arguments. The concern separation works both ways: I don't touch their images, they don't constrain mine. And the host OS surely won't know any better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs along the way
&lt;/h2&gt;

&lt;p&gt;I found a total of one trade-off: I lost Longhorn.&lt;/p&gt;

&lt;p&gt;Longhorn is the right persistent storage story for K3s. It's also sitting behind an iSCSI requirement, which means kernel modules — which means I'd need to build a custom &lt;code&gt;longhorned-k3s&lt;/code&gt; image that has the right binaries, reaches into the host kernel and hopes it guessed right. That image would be mine to maintain forever, against a scratch base I can't easily inspect or extend.&lt;/p&gt;

&lt;p&gt;This is, ironically, the exact trap the whole setup was designed to avoid. So I didn't do it. &lt;code&gt;/data&lt;/code&gt; on the block volume is fine for a homelab. Local-path PVCs do the job.&lt;/p&gt;

&lt;p&gt;The headache of homelab infrastructure is supposed to be &lt;em&gt;fun&lt;/em&gt; headache. There's a line between "productive friction you learn from" and "work you do instead of the actual thing." Longhorn&lt;br&gt;
crossed that line. I cut it. If this were production, I'd be on EKS and none of this would exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ephemerality Project is a Success
&lt;/h2&gt;

&lt;p&gt;It takes 2 minutes and 30 seconds from the second Oracle Cloud finishes creating a VM to ArgoCD being fully deployed and deploying my root app.&lt;/p&gt;

&lt;p&gt;That's cloud-init, Podman starting the steward, Tailscale coming up, K3s initializing, the API ready, bootstrap done, ArgoCD CRDs registered, root app deployed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;preserve_boot_volume = false&lt;/code&gt; in Terraform. I genuinely don't care if Oracle recycles the boot volume. Everything stateful is on the block volume. Everything ephemeral is in the image. The VM is cattle. That was the original promise.&lt;/p&gt;

&lt;p&gt;It delivered. I just had to actually follow the logic through.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The host OS matters exactly once&lt;/strong&gt;: getting your first container running. Pick one that gets out of the way immediately. Ubuntu Minimal does this. SELinux-enforcing distros don't — not because they're wrong, but because they conflict with the premise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't extend scratch images. Use them.&lt;/strong&gt; If an upstream image is minimal or scratch, that's a signal. Compose around it, don't modify it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One container per concern.&lt;/strong&gt; The steward pattern — a thin orchestrator managing purpose-built upstream images — is what container thinking actually looks like. "One container for the whole machine" is just a VM with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know when to cut.&lt;/strong&gt; Not every yak needs shaving. Longhorn would be fun. Longhorn would also be a project. This is a homelab.&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@pcalescu" rel="noopener noreferrer"&gt;Paul Calescu&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>linux</category>
    </item>
    <item>
      <title>Containers, The Wrong Way, For Always-Free Fun and Profit</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Mon, 23 Mar 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/notjustanna/containers-the-wrong-way-for-always-free-fun-and-profit-3ea1</link>
      <guid>https://dev.to/notjustanna/containers-the-wrong-way-for-always-free-fun-and-profit-3ea1</guid>
      <description>&lt;h2&gt;
  
  
  Prelude: Oracle Cloud's Always-Free Tier
&lt;/h2&gt;

&lt;p&gt;Oracle Cloud has an always-free tier. Not a trial. Not "free for 12 months." Always free.&lt;/p&gt;

&lt;p&gt;Four ARM-based cores, 24GB of RAM, 200GB of storage. For nothing. Forever.&lt;/p&gt;

&lt;p&gt;Their Ampere Altra processors are genuinely good silicon. People benchmark these against x86 and come away impressed. And the ARM64 ecosystem is in good shape — most container images you'll actually use (your databases, your ingress controllers, your monitoring stack) have ARM64 builds. The days of "does this even run on ARM" are mostly behind us.&lt;/p&gt;

&lt;p&gt;The fact that Oracle is giving this hardware away to get people onto their platform is, frankly, their problem.&lt;/p&gt;

&lt;p&gt;If you have any kind of homelab itch — self-hosted apps, a personal Kubernetes playground, a place to run things you don't want living on your laptop — you should have one of these VMs. The barrier is a credit card for verification (they won't charge it) and about twenty minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The VM
&lt;/h2&gt;

&lt;p&gt;Once you have an account, getting a VM up is a few clicks in the OCI console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute → Instances → Create Instance&lt;/li&gt;
&lt;li&gt;Change the shape to &lt;strong&gt;VM.Standard.A1.Flex&lt;/strong&gt; — that's the ARM one&lt;/li&gt;
&lt;li&gt;Set OCPUs to 4 and memory to 24GB (max free allocation)&lt;/li&gt;
&lt;li&gt;Pick your image.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oracle Linux&lt;/strong&gt; if you're comfortable with &lt;code&gt;dnf&lt;/code&gt;. &lt;strong&gt;Ubuntu Server&lt;/strong&gt; if you're a &lt;code&gt;apt&lt;/code&gt; person.&lt;/li&gt;
&lt;li&gt;Either works — both have Minimal variants that strip out a lot of packages out. And if you're about to do what I'm about to describe, you'll want the Minimal version.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add your SSH key&lt;/li&gt;
&lt;li&gt;Create&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll also want a separate block volume: Storage → Block Volumes → Create, 150GB, attach it to your instance.&lt;/p&gt;

&lt;p&gt;If you're Terraform-inclined, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"oci_core_instance"&lt;/span&gt; &lt;span class="s2"&gt;"my-arm-instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;shape&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VM.Standard.A1.Flex"&lt;/span&gt;
  &lt;span class="nx"&gt;shape_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ocpus&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="nx"&gt;memory_in_gbs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;source_details&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;source_type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"image"&lt;/span&gt;
    &lt;span class="nx"&gt;source_id&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oracle_linux_minimal_image_id&lt;/span&gt;
    &lt;span class="nx"&gt;boot_volume_size_in_gbs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;  &lt;span class="c1"&gt;# OCI minimum&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"oci_core_volume"&lt;/span&gt; &lt;span class="s2"&gt;"data"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# this is where anything you'd like to still exist tomorrow lives&lt;/span&gt;
  &lt;span class="nx"&gt;size_in_gbs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Free Kubernetes. Kinda.
&lt;/h2&gt;

&lt;p&gt;K3s goes up in about thirty seconds:&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;-sfL&lt;/span&gt; https://get.k3s.io | sh
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a single-node Kubernetes cluster. One control plane, one worker, same machine. It's not highly available — if the VM goes down, your cluster goes down.&lt;/p&gt;

&lt;p&gt;If you want something closer to HA, OCI's free tier technically allows you to split your 4 OCPUs and 24GB across multiple VMs — you could do three VMs at 1 OCPU / 8GB each (control pane gets 2 OCPUs) and run a proper multi-node setup with an embedded etcd quorum. That's a valid path. This post is about the single-node case because I don't care about any of that and neither should you for a homelab.&lt;/p&gt;

&lt;p&gt;The point is: Kubernetes, on ARM, for free. This feels unreasonable but here we are.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part Where You Discover You Have a Long-Lived Server Problem
&lt;/h2&gt;

&lt;p&gt;Here's the thing about a cloud VM: you don't touch it daily like your laptop or your desktop. You SSH into it when something breaks, or when you need to add a new service. If you're running Kubernetes, you SSH in even less — &lt;code&gt;kubectl&lt;/code&gt; handles most of it and you rarely need to touch the node directly.&lt;/p&gt;

&lt;p&gt;But the machine keeps running. Packages drift out of date. You hotfixed something at 2am while sick and half-forgot. You're not sure which K3s version you're actually on or whether it's the one you meant to be running. The machine accumulates entropy in the background while you're not looking.&lt;/p&gt;

&lt;p&gt;This is the long-lived server problem and it's why configuration management tools exist. The standard answer: write an Ansible playbook, push it to git, have the machine pull and run it on a schedule. Define the desired state, let Ansible converge to it.&lt;/p&gt;

&lt;p&gt;So you write the playbook. It works. You commit it to git, set up a cron job on the VM to pull and run it every five minutes, and declare victory.&lt;/p&gt;

&lt;p&gt;And then you start noticing the friction.&lt;/p&gt;

&lt;p&gt;Ansible is... &lt;em&gt;fine&lt;/em&gt; for keeping a server configured. But what you're increasingly fighting is a different problem — you want something closer to what &lt;code&gt;apt upgrade&lt;/code&gt; does, but for your entire environment. Not "apply these tasks," but "this is the version of the world I want, please be it." Ansible can do it but it's not really what it's designed for, and you can feel the difference. The playbook describes a path to the desired state, not the desired state itself. Those are subtly different things and the difference starts to matter when you're maintaining the playbook over months.&lt;/p&gt;

&lt;p&gt;The other problem: there's no local testing story. You make a change, push to git, wait for the cron job, SSH in to see if anything broke. Your laptop is not a Linux ARM server with K3s running on it. You can't just run the playbook locally and catch problems before they hit the VM.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thought That Won't Go Away
&lt;/h2&gt;

&lt;p&gt;I work with Kubernetes other every day. Kubernetes runs containers. Containers are versioned, immutable artifacts — you build one, push it to a registry, pull it somewhere else, it behaves exactly the same. You can run it locally to test it. You update it by pushing a new version. Rolling back means pulling an old tag.&lt;/p&gt;

&lt;p&gt;Everything about this model is better than a long-lived server managed by a configuration management tool.&lt;/p&gt;

&lt;p&gt;And somewhere around the third time I was debugging why the playbook had done something unexpected, I had the thought: &lt;em&gt;why can't I just containerize this entire problem?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;K3s is not a web app. K3s needs to interact with the host kernel — manipulate iptables, create network interfaces, manage cgroups, set up container networking. You can't just &lt;code&gt;docker run k3s&lt;/code&gt; and expect it to work.&lt;/p&gt;

&lt;p&gt;Or... can you?&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;--privileged&lt;/code&gt; flag
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--privileged&lt;/code&gt; is Docker and Podman's "I know what I'm doing" flag. It gives the container essentially full access to the host kernel — every capability, every device, no security filtering. It's the nuclear option.&lt;/p&gt;

&lt;p&gt;And it turns out, it's also the officially documented way to run K3s in a container. From K3s's own docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; k3s-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 6443:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; rancher/k3s:v1.29.3-k3s1 &lt;span class="se"&gt;\&lt;/span&gt;
  server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's in the K3s documentation. Not a workaround. Not a hack. &lt;code&gt;--privileged&lt;/code&gt; is how you do this.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;--network host&lt;/code&gt; gives the container the host's network stack directly, which K3s needs to set up its own networking correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/you/k3s-env:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does this feel wrong? Yes. A privileged container with host networking is basically just a process. Security people will say this.  They're not wrong.&lt;/p&gt;

&lt;p&gt;But it's a process defined by an OCI image. Which means it has a version tag. A Dockerfile in a git repo. A build pipeline. And — crucially — you can &lt;code&gt;docker run&lt;/code&gt; it on your laptop and test it before it touches anything real.&lt;/p&gt;

&lt;p&gt;The trade is: you give up container isolation (which K3s was going to make you give up anyway to do its job) and you get everything else that comes with the container model. That's a good trade.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Image
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; rancher/k3s:v1.29.3-k3s1&lt;/span&gt;

&lt;span class="c"&gt;# Whatever else you want running alongside K3s&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; tailscale

&lt;span class="c"&gt;# K3s auto-deploys anything placed here on startup&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; argocd-install.yaml /var/lib/rancher/k3s/server/manifests/argocd.yaml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; root-app.yaml /var/lib/rancher/k3s/server/manifests/root-app.yaml&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; entrypoint.sh /entrypoint.sh&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/entrypoint.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rancher/k3s&lt;/code&gt; as the base means K3s is pre-installed at a specific pinned version. K3s has an auto-deploy feature: anything in &lt;code&gt;/var/lib/rancher/k3s/server/manifests/&lt;/code&gt; gets automatically applied when K3s starts. Drop ArgoCD's install manifest in there, point ArgoCD at a git repo, and everything else deploys itself from git. The image doesn't need to know about any of it.&lt;/p&gt;

&lt;p&gt;The entrypoint starts things in the right order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Secrets are in /data/env, written at startup&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; /data/env

&lt;span class="c"&gt;# Start networking, wait for it to be ready&lt;/span&gt;
tailscaled &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/tailscale.state &amp;amp;
tailscale up &lt;span class="nt"&gt;--authkey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TAILSCALE_AUTHKEY&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;tailscale status&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# K3s in the foreground — keeps the container alive&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;k3s server &lt;span class="nt"&gt;--data-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/rancher/k3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Oracle Linux's New Job Description
&lt;/h2&gt;

&lt;p&gt;A small shell script runs at VM startup (wired up via systemd) and does exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; podman
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /data
mount /dev/sdb /data

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /data/env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
TAILSCALE_AUTHKEY=your-key-here
OTHER_SECRET=whatever
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /data/env

podman run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; /data/env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; k3s-env &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/you/k3s-env:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five commands. Run once on first boot. That is the entire configuration management story for Oracle Linux.&lt;/p&gt;

&lt;p&gt;After that, Oracle Linux's responsibilities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping Podman alive&lt;/li&gt;
&lt;li&gt;Applying its own package updates at 3am via &lt;code&gt;dnf-automatic&lt;/code&gt; (or &lt;code&gt;unattended-upgrades&lt;/code&gt; on Ubuntu) — install it, enable the timer, forget about it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Oracle Linux is now a very fancy process supervisor. It doesn't know what's running inside the container and doesn't need to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part That Makes It Worth It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can test it locally.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /tmp/test-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.test &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/you/k3s-env:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your entire server environment, running on your laptop. K3s comes up, deploys things, you poke at it with kubectl. You find the problem before it touches the VM. This was impossible with Ansible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updates are a push.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Change anything in the Dockerfile — update the K3s version, add a package, update a manifest — build a new image tag, push to your registry. A systemd timer on the VM checks for image updates at 3am:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman pull ghcr.io/you/k3s-env:latest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; podman restart k3s-env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No SSH. Noscottrodgerson playbook run. No waiting for convergence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The boot volume can die and you don't care.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything stateful — K3s cluster state, persistent volumes, all of it — lives at &lt;code&gt;/data&lt;/code&gt; on the separate block volume, mounted into the container. Oracle can update or replace the boot volume. Run the startup script, the container comes back up, finds its state on &lt;code&gt;/data&lt;/code&gt;, continues exactly where it left off.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is This "Correct"?
&lt;/h2&gt;

&lt;p&gt;No. Using &lt;code&gt;--privileged&lt;/code&gt; is not what containers are designed for. Running K3s inside a container on a VM where you could just run K3s directly is adding a layer that doesn't need to be there from a pure architecture standpoint.&lt;/p&gt;

&lt;p&gt;But "architecturally pure" and "actually useful for your situation" are different questions. This approach gives you a reproducible, testable, versionable environment on a server you interact with twice a year. The feedback loop goes from "push to git, wait for cron, SSH in and hope" to "docker run on your laptop, push when it works."&lt;/p&gt;

&lt;p&gt;For a free ARM Kubernetes node that you barely touch, that's the right trade.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;--privileged&lt;/code&gt; is in the K3s docs. So maybe it's fine.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@pcalescu" rel="noopener noreferrer"&gt;Paul Calescu&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>oracle</category>
      <category>linux</category>
    </item>
    <item>
      <title>Thinking Differently About Universal Microkernels</title>
      <dc:creator>Anna Silva</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:33:11 +0000</pubDate>
      <link>https://dev.to/notjustanna/thinking-differently-about-universal-microkernels-1hd0</link>
      <guid>https://dev.to/notjustanna/thinking-differently-about-universal-microkernels-1hd0</guid>
      <description>&lt;p&gt;As a not-Macbook owner, I have to yell this into the void: I really like macOS' kernel. Not macOS — XNU. The kernel underneath.&lt;/p&gt;

&lt;p&gt;XNU's a hybrid microkernel — meaning most of the OS lives in userspace rather than baked into the kernel itself. Device drivers? Architecturally designed to be userspace programs, even if Apple doesn't always do it that way. Crash a device driver, you crash the device driver. Not the whole system. The kernel stays up. The kernel doesn't care.&lt;/p&gt;

&lt;p&gt;Compare that to Linux, where a buggy driver can take down the entire machine because it's all in the same address space, with the same privileges, one bad pointer away from a kernel panic.&lt;/p&gt;

&lt;p&gt;XNU's model is just... better in every way. Cleaner. The kernel does the minimum, as the SOLID gods stated on their Single Responsibility Commandment. Everything else is up to the rest of the operating system. XNU is an actual kernel rather than a cob of corn — which in Linux's case, is one fire away from exploding into popcorn.&lt;/p&gt;

&lt;p&gt;And I would love this on my CachyOS with COSMIC desktop. I should be allowed to have bad taste in desktop environment but good taste in kernel architecture. I want my drivers isolated. I want to run whatever userspace I feel like, on x86 or ARM, and not particularly care which one I'm on. I want something like Apple's Universal Binary — one thing that runs everywhere — except &lt;em&gt;actually&lt;/em&gt; universal, not "universal between the two architectures Apple currently sells."&lt;/p&gt;

&lt;p&gt;So Anyway, I Started &lt;del&gt;Blasting&lt;/del&gt; Googling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Does Only Apple Get A Decent Kernel?
&lt;/h2&gt;

&lt;p&gt;Actually...&lt;/p&gt;

&lt;p&gt;Microkernels aren't a new idea. They're not even an "Think Different" idea. They've been the "obviously correct" architecture in OS research since the 1980s. The theory is sound: small trusted kernel, everything else in userspace, hardware isolation enforces the boundaries. Fewer things can go wrong. The things that do go wrong are contained.&lt;/p&gt;

&lt;p&gt;And yet the consumer OS landscape is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS/iOS&lt;/strong&gt; — XNU, hybrid microkernel ✓&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; — monolithic-ish NT kernel, practically speaking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt; — monolithic, famously so&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt; — Linux underneath, with a Java runtime on top&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Everything else is too niche to matter for this conversation. For now. We'll get back to it.)&lt;/p&gt;

&lt;h3&gt;
  
  
  And WHY didn't microkernels win?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The honest answer is performance.&lt;/strong&gt; Which is funny, because the company that makes the only mainstream microkernel OS also makes the fastest consumer laptops on the market right now. Apple proved microkernels can be fast. Apple also makes it impossible to run anything else on their hardware. Make it make sense.&lt;/p&gt;

&lt;p&gt;But historically: early microkernel implementations (Mach, which XNU is partly based on) were slow because crossing the kernel/userspace boundary has overhead, and if your networking stack and filesystem are both in userspace, every I/O operation crosses that boundary multiple times. L4, a later microkernel, proved you could make those crossings fast enough to matter. But by then Linux had momentum and "fast enough" wasn't enough to displace it.&lt;/p&gt;

&lt;p&gt;So we ended up in a world where the kernel architecture that makes more engineering sense, runs on the fastest and most efficient laptop hardware money can buy... which can only be obtained from the Cupertino company.&lt;/p&gt;




&lt;h2&gt;
  
  
  "The Universal Binary"-sized Elephant in the Room
&lt;/h2&gt;

&lt;p&gt;So while I was down this rabbit hole admiring XNU's architecture, I figured I'd look at the Universal Binary thing more closely. They're neat. The idea that you can ship one file that Just Works™ on both x86 and ARM is exactly the kind of "computers should be less annoying" energy I respect.&lt;/p&gt;

&lt;p&gt;I assumed they were doing something clever under the hood. Like shipping LLVM IR and compiling to native on first run. Late-binding optimization. Your binary gets smarter when it lands on new hardware.&lt;/p&gt;

&lt;p&gt;Reader, &lt;em&gt;they are not doing that&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A Universal Binary is a fat binary. It contains the x86-64 version of your app. And the ARM64 version of your app. Bundled together. The OS picks the right one at launch and ignores the other half.&lt;/p&gt;

&lt;p&gt;You're shipping two binaries in a trench coat.&lt;/p&gt;

&lt;p&gt;The optimization is frozen at compile time. When Apple releases a new chip, your binary doesn't get smarter. It just... runs the ARM slice, which was compiled for a generic ARM target, not &lt;em&gt;your specific chip&lt;/em&gt;, not &lt;em&gt;your cache hierarchy&lt;/em&gt;, not anything about the actual hardware underneath it.&lt;/p&gt;

&lt;p&gt;It's the right solution to the wrong problem. It solves "how do we run x86 apps on ARM during a transition period." It does not solve "how do we write software once and have it run optimally everywhere, forever."&lt;/p&gt;

&lt;p&gt;I wanted the deeper solution. LLVM IR would have solved this — compile to IR, ship the IR, recompile for whatever hardware you're actually running on. Late-binding optimization. Your binary gets smarter when Apple releases a new chip. For free.&lt;br&gt;
Apple literally makes the compiler that could do this. They maintain Clang. They built their own LLVM-based toolchain. They had all the pieces.&lt;br&gt;
Why???&lt;/p&gt;




&lt;h2&gt;
  
  
  Someone Already Tried This. Several Someones, Actually.
&lt;/h2&gt;

&lt;p&gt;Let's set the microkernel idea on the shelf for a moment and look at the universal binary problem separately. They're going to converge, I promise. But before we get to what I think the answer is — for both — it's worth knowing that this problem has a graveyard.&lt;/p&gt;

&lt;p&gt;Microsoft Research built &lt;strong&gt;Singularity&lt;/strong&gt; in 2003 — an entire OS written in managed C#, where the type system replaced hardware memory protection. That's not a metaphor. The language verifier was doing the job the MMU normally does. Load-bearing C#, if you will.&lt;/p&gt;

&lt;p&gt;It evolved into a project called Midori, got far enough to run Microsoft's search infrastructure in production, and was quietly killed in 2015. Too much to ask the world to abandon their existing software ecosystem for a managed-code utopia. Graveyard, plot one.&lt;/p&gt;

&lt;p&gt;Then, Google built &lt;strong&gt;Fuchsia&lt;/strong&gt; — a capability-based microkernel OS with proper driver isolation, a real component model, everything done right. It shipped briefly on the Nest Hub smart display. Then got rolled back to Linux. Now it exists in a state of quantum superposition between "advanced research" and "we'll get to sunsetting that eventually."&lt;/p&gt;

&lt;p&gt;The pattern is consistent: build the right thing, hit the ecosystem wall, die.&lt;/p&gt;

&lt;p&gt;But there's one entry in this space that &lt;em&gt;didn't&lt;/em&gt; die, and it's interesting because of how it survived.&lt;/p&gt;




&lt;h2&gt;
  
  
  eBPF: the idea that snuck in sideways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;eBPF&lt;/strong&gt; is nominally a "packet filtering" system in the Linux kernel. The name stands for "extended Berkeley Packet Filter" — very boring, sounds like a networking detail, easy to ignore.&lt;/p&gt;

&lt;p&gt;It isn't a networking detail.&lt;/p&gt;

&lt;p&gt;Here's what eBPF actually is: a bytecode format, a verifier, and a JIT compiler living inside the Linux kernel. You write a program, the verifier checks that it's safe (no unbounded loops, no invalid memory access, all paths terminate), and then it runs in kernel space with near-zero overhead. No ring transitions. No syscall overhead. Just verified code running at the most privileged level because the verifier already proved it can't do anything wrong.&lt;/p&gt;

&lt;p&gt;That's the Singularity bet — type safety replacing hardware protection — except applied narrowly enough that nobody objected to merging it into mainline Linux.&lt;/p&gt;

&lt;p&gt;And the scope of what eBPF can do keeps expanding: network processing, system call filtering, security policy enforcement, TCP congestion control, and now — as of recent Linux versions — &lt;strong&gt;writing CPU schedulers&lt;/strong&gt;. Userspace-authored, verifier-checked code making scheduling decisions in ring 0. Meta runs their entire production infrastructure networking on eBPF. Cloudflare's DDoS mitigation runs on eBPF.&lt;/p&gt;

&lt;p&gt;eBPF is the VM-as-OS idea that actually shipped at scale. It just wore a disguise.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebAssembly changes the equation
&lt;/h2&gt;

&lt;p&gt;Here's where I think things get interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebAssembly (WASM)&lt;/strong&gt; is a bytecode format originally designed for running code in browsers at near-native speed. That's its origin story. That's what it says on the tin.&lt;/p&gt;

&lt;p&gt;I'm not interested in what it says on the tin.&lt;/p&gt;

&lt;p&gt;WASM is defined to be safe by spec. No arbitrary pointer arithmetic. No unverified control flow. Memory accesses are bounds-checked. The verifier is part of the standard. This means: if you have a WASM runtime embedded in your kernel, and you load a driver as a WASM module, the verifier checks the driver before a single instruction executes. The driver cannot, by construction, corrupt memory it wasn't given access to.&lt;/p&gt;

&lt;p&gt;If your brain works in the particular weird way that mine does, something just clicked together that probably shouldn't have. XNU isolates drivers through hardware address space separation. WASM isolates modules through verified bytecode. These are the same guarantee. One costs a ring transition. The other costs a verifier pass at load time.&lt;/p&gt;

&lt;p&gt;You could just. Use WASM. As the driver sandbox. Instead of the MMU.&lt;/p&gt;

&lt;p&gt;And while we're at it — why stop at drivers?&lt;/p&gt;

&lt;p&gt;WASI (the WebAssembly System Interface) exists precisely to run system-level code in WASM. It's POSIX, but typed. Capability-gated. You declare what your module needs — filesystem access, network access, memory-mapped I/O — and the host grants exactly that. Nothing undeclared is accessible. Not "restricted." Not "monitored." Just. Not there.&lt;/p&gt;

&lt;p&gt;That's not a driver sandbox. That's an entire OS component model.&lt;/p&gt;

&lt;p&gt;A network stack as a WASM component that imports &lt;code&gt;wasi-sockets&lt;/code&gt;. A filesystem driver that imports &lt;code&gt;wasi-filesystem&lt;/code&gt;. A display server that imports &lt;code&gt;wasi-gpu&lt;/code&gt; or whatever we'd call it. Each one verified before it runs. Each one incapable of touching what it didn't declare. Each one replaceable without touching anything else.&lt;/p&gt;

&lt;p&gt;XNU does this with hardware isolation and a bespoke driver framework. WASM does this with a verifier and an interface definition file.&lt;/p&gt;

&lt;p&gt;The Cupertino company spent decades building the infrastructure for this. We have a W3C spec and a Rust library.&lt;/p&gt;

&lt;p&gt;These are not as different as they sound.&lt;/p&gt;




&lt;h2&gt;
  
  
  Great idea. Where do we get drivers? Where do we get apps?
&lt;/h2&gt;

&lt;p&gt;Here's where the LLVM thing becomes important: the drivers and applications are just waiting to be recompiled.&lt;/p&gt;

&lt;p&gt;WASM is a valid LLVM target. LLVM is the backend that powers Clang, Rust, Swift, Kotlin Native, and most modern compiled languages. Which means any language that compiles through LLVM can emit WASM. Not as an afterthought. As a flag you pass to the compiler.&lt;/p&gt;

&lt;p&gt;So when someone asks "who writes the drivers for your WASM microkernel" — the answer is nobody. They're already written. In C. Sitting in the Linux kernel tree. Twenty years of accumulated hardware knowledge, weird edge cases, datasheets that lied, and fixes in comments nobody has read since 2009.&lt;/p&gt;

&lt;p&gt;You don't rewrite them. You recompile them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clang --target=wasm32-wasi driver.c&lt;/code&gt;. The Linux driver doesn't know it isn't on Linux. It asked for memory-mapped I/O access. It got a WASI capability that provides memory-mapped I/O access. Same semantic. Different implementation. Verified safe by construction. Sandboxed by the verifier before a single instruction executes.&lt;/p&gt;

&lt;p&gt;Is this emulation? No. There's no semantic gap being papered over. It's just compilation with an intermediate stop that happens to give you safety, portability, and late-binding optimization as byproducts.&lt;/p&gt;

&lt;p&gt;Late-binding as in: your kernel stores the WASM bytecode. First boot on new hardware, it recompiles everything with the LLVM backend targeting your actual CPU. AVX-512. Your specific cache hierarchy. Your branch predictor. Your five-year-old driver binary gets Zen 5 optimizations its author never knew existed.&lt;/p&gt;

&lt;p&gt;Apple ships two frozen binaries in a trench coat. This ships one bytecode and derives the optimal binary at runtime, on your hardware, for free.&lt;/p&gt;

&lt;p&gt;Universal Binary was the right idea. This is the actual implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what would this actually look like?
&lt;/h2&gt;

&lt;p&gt;A Rust microkernel — Rust because memory safety in the kernel itself matters, and because Rust has excellent embedded/bare-metal support. A small, trusted core: interrupt handling, capability-based IPC, a scheduler, physical memory management. As little as possible.&lt;/p&gt;

&lt;p&gt;A WASM runtime (Wasmtime is embeddable as a Rust library, this is its designed use case) handling module loading, verification, and JIT compilation via Cranelift for startup speed and the LLVM backend for optimizing hot paths.&lt;/p&gt;

&lt;p&gt;WASI as the system interface. Drivers and kernel modules are WASM components that declare their capability imports. The kernel grants capabilities at load time. A network driver imports networking hardware access. It doesn't get filesystem access. It can't even ask for it.&lt;/p&gt;

&lt;p&gt;A persistent module store: compiled WASM cached as native artifacts per hardware profile. Recompiled when the hardware changes. Profile-guided optimization over time as the system learns which paths are hot.&lt;/p&gt;

&lt;p&gt;POSIX compatibility as a WASM component itself — a userspace layer, not baked into the kernel. You want Linux semantics? Load the POSIX compatibility module. You want something else? Load something else. The kernel doesn't care.&lt;/p&gt;

&lt;p&gt;The result: your COSMIC desktop, or KDE, or whatever you want, running on top of a clean microkernel with isolated drivers, on x86 or ARM, with code that gets better at running on your specific hardware over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The wall
&lt;/h2&gt;

&lt;p&gt;And... here's where I have to be honest.&lt;/p&gt;

&lt;p&gt;This requires WebAssembly to become a serious systems target, not just a browser and edge-compute story. That's happening — slowly. The Bytecode Alliance (Mozilla, Microsoft, Fastly, Intel, Red Hat) is doing real work on WASI and the Component Model. Wasmtime is production quality-ish. The pieces exist.&lt;/p&gt;

&lt;p&gt;But "the pieces exist" is a long way from "the ecosystem exists." Linux driver authors aren't thinking about WASM targets. Systems programmers aren't writing kernel modules in WASM-first workflows. The toolchain integration is immature. The debugging story is rough.&lt;/p&gt;

&lt;p&gt;Meanwhile the Android black hole keeps pulling everything in. Amazon just shipped Vega OS — their escape from Android's GPL gravity — and their solution to "what's the application runtime" was JavaScript. React Native on Linux. They escaped the JVM and landed in the V8 engine. Different VM, same fundamental bet, worse type system. The ecosystem gravity is so strong that even the companies with resources to build something better keep reinventing 1996 with a different runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm not defeated about this
&lt;/h2&gt;

&lt;p&gt;While we were busy eulogizing Singularity and Fuchsia, something quietly happened to Linux.&lt;/p&gt;

&lt;p&gt;Someone snuck eBPF in. It now handles networking, security policy, system call filtering, TCP congestion control, and as of recent kernel versions — CPU scheduling. Someone wrote a CPU scheduler in eBPF. It merged. Linus signed off on it.&lt;/p&gt;

&lt;p&gt;And separately, people are smuggling Rust into the kernel. Driver by driver. Not a rewrite — a slow infiltration. Memory-safe, verifiable, LLVM-native code quietly becoming acceptable in the codebase that's been C since before most of us were born.&lt;/p&gt;

&lt;p&gt;The monolithic cob of corn is being hollowed out. Slowly. Commit by commit. By people with CVEs on their conscience and patience measured in decades.&lt;/p&gt;

&lt;p&gt;Maybe Linux gets back to being kernel-sized someday. It wouldn't be the first time an idea took forty years to arrive.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@lukasz_rawa" rel="noopener noreferrer"&gt;Łukasz Rawa&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webassembly</category>
      <category>kernel</category>
      <category>operatingsystems</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
