<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>HRNPH BLOG</title><description>A personal blog of hrnph (hirunkul phimsiri)</description><link>https://blog.hrnph.dev/</link><language>en</language><item><title>Designing Multi-Tenancy for a B2B SaaS Healthcare Startup</title><link>https://blog.hrnph.dev/posts/designing-multi-tenancy-b2b-saas-healthcare/</link><guid isPermaLink="true">https://blog.hrnph.dev/posts/designing-multi-tenancy-b2b-saas-healthcare/</guid><description>Why I picked a silo (single-tenant stack per customer) for a healthcare B2B product, what it costs, and how to choose a tenancy model for your own SaaS. Same codebase, per-tenant isolation, one control plane that tenants call home to.</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;Every healthcare B2B sale I&apos;ve been in reaches the same moment. The demo goes fine, the doctors like it, and then the hospital IT director leans back and asks:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;So where exactly does our patient data live?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Whatever you answer in the next thirty seconds decides the deal faster than any feature you showed them.&lt;/p&gt;
&lt;p&gt;This post is about the tenancy design I ended up with for our rehab platform: a silo model, where every hospital customer gets its own full stack (own database, own cache, own storage, own network), all running the same codebase, with a central control plane watching over all of them. I&apos;ll walk through why I picked it, what it costs, and how to reason about the choice for your own SaaS, because the right answer for you is probably different.&lt;/p&gt;
&lt;p&gt;:::note
This is an engineering perspective from building one product. I&apos;m not claiming silo is universally correct. It isn&apos;t. There&apos;s a whole section at the end about when you should not do this.
:::&lt;/p&gt;
&lt;h1&gt;What &quot;multi-tenancy&quot; actually means in practice&lt;/h1&gt;
&lt;p&gt;People throw the word around like it&apos;s one thing. It&apos;s a spectrum, and where you sit on it decides most of your operational life.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pooled.&lt;/strong&gt; One database, one app, every row has a &lt;code&gt;tenant_id&lt;/code&gt; column. Cheapest to run. But every query you ever write carries &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; forever, and the one time somebody forgets it, you&apos;ve leaked one hospital&apos;s patients into another hospital&apos;s screen. One bad migration takes down every customer simultaneously, in the same minute, on the same outage page. Restoring a single customer&apos;s data means surgically extracting it from a live shared database, which nobody enjoys at 2am.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bridge.&lt;/strong&gt; Shared cluster, database (or schema) per tenant. Middle ground. Isolation at the data layer, shared everything else. Smaller blast radius, still shared compute and shared meltdowns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Silo.&lt;/strong&gt; Every tenant gets the whole stack: their own database, their own cache, their own object storage, their own containers. Most expensive per tenant. Also the only model where &quot;this hospital&apos;s data&quot; has a physical answer you can point at.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./0-spectrum.svg&quot; alt=&quot;The tenancy spectrum: pooled, bridge, and silo shapes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I ended up at silo.&lt;/p&gt;
&lt;h1&gt;How to pick a model for your SaaS&lt;/h1&gt;
&lt;p&gt;These are the questions I actually asked. In rough order of how much they mattered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. What does your worst day look like?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Write down the sentence &quot;we leaked hospital A&apos;s patient records to hospital B&quot; and imagine sending it to your customers. In healthcare that sentence ends the company. Not hurts it, ends it. PDPA liability, hospital contracts, reputation, done. If your worst day is &quot;some users saw a slower dashboard&quot;, pooled is fine and you should save the money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Who decides to buy?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consumer SaaS: users decide, in seconds, and churn in seconds. B2B healthcare: an IT director and a director of medicine decide, over months, and the deployment review is part of the product. When the buyer is the person asking &quot;where does our data live&quot;, isolation is a sales feature, not overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Will you ever need to run on the customer&apos;s infrastructure?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We knew the answer was eventually yes. Some hospitals want (or procurement requires) the system running inside their own network, on their own metal. That requirement forces single-tenant-shaped deployment whether you like it or not, so your architecture has to be a single-tenant unit you can stamp out N times anyway. Once that&apos;s true, &quot;cloud&quot; and &quot;on-prem&quot; become the same artifact deployed in different places, which is exactly what we wanted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Can your team operate N stacks?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the real cost gate. If the answer is no and you can&apos;t automate your way to yes, silo will eat you. More on that in the costs section.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5-picking-flow.svg&quot; alt=&quot;The picking flow as a decision tree from worst day to tenancy model&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Why silo won for us&lt;/h1&gt;
&lt;p&gt;The product is a rehab exercise platform: doctors prescribe exercise programs, patients do them through a LINE mini app with on-device pose detection, doctors watch progress. B2B, hospitals and clinics, Thailand.&lt;/p&gt;
&lt;p&gt;Four things pushed me to silo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;We&apos;re B2B and every customer runs the same codebase.&lt;/strong&gt; No hospital needs a custom fork. So the unit of deployment could be &quot;the whole product, once per customer&quot; with zero product divergence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Healthcare people are correctly paranoid.&lt;/strong&gt; Thai hospitals care about PDPA, about data residency, about who can see what. The sentence &quot;your data lives in your own Postgres, on a machine you can point at, and no other hospital&apos;s data has ever been in it&quot; does more for a deal than any roadmap slide. Call it easing geezer minds if you want; the geezers sign the contracts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blast radius.&lt;/strong&gt; One tenant&apos;s runaway job, one botched migration, one noisy neighbor. It&apos;s that tenant&apos;s problem, at that tenant&apos;s pace. Everyone else doesn&apos;t know it happened.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-prem had to exist eventually&lt;/strong&gt;, and as question 3 above says, that forces the shape anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There was also a quieter reason, operational sanity. Per-tenant backup and restore is a whole-stack snapshot, not surgery. Per-tenant upgrade is a rolling decision per customer. If a hospital wants to stay on an older version for a month while their IT reviews, that&apos;s their stack, their choice, no feature flags required.&lt;/p&gt;
&lt;h1&gt;The architecture&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;One codebase, one container image, stamped out once per tenant, plus a central control plane&lt;/strong&gt; (&quot;the tower&quot;) that provisions and monitors all of them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1-architecture.svg&quot; alt=&quot;The silo architecture: one control tower, N identical tenant stacks calling home&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each tenant stack is a compose deployment with its own Postgres, Redis, object storage, the API, the doctor dashboard, and the patient mini app, all inside a Docker network named after the tenant. Nothing inside that network needs to talk to another tenant, ever. The tenant gets its own domain pair (API and app), its own TLS, its own everything.&lt;/p&gt;
&lt;h2&gt;The detail that makes on-prem work: tenants call home, the tower never dials in&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The control plane never initiates connections to tenants.&lt;/strong&gt; Every communication is tenant-initiated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each tenant sends a &lt;strong&gt;heartbeat&lt;/strong&gt; every 60 seconds (with exponential backoff if the tower is unreachable). The heartbeat carries health status and metrics.&lt;/li&gt;
&lt;li&gt;The tower&apos;s response to a heartbeat is where &lt;strong&gt;commands ride back&lt;/strong&gt;: &quot;push the latest exercise catalog&quot;, &quot;rotate your key&quot;, &quot;force a heartbeat now&quot;. Commands piggyback on a connection the tenant already opened.&lt;/li&gt;
&lt;li&gt;Tenants push &lt;strong&gt;aggregate stats&lt;/strong&gt; hourly: counts only, no personal data ever leaves the tenant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./2-call-home.svg&quot; alt=&quot;How tenants call home: heartbeat loop, commands riding back on the response, hourly stats&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A tenant deployed inside a hospital network is behind NAT, behind firewalls, in rooms you will never see. If your control plane needs to reach in, on-prem is dead on arrival. If tenants call out, the same artifact works in your cloud, in the customer&apos;s datacenter, and in a hospital basement with one outbound rule. HTTP(S) outbound only.&lt;/p&gt;
&lt;p&gt;:::tip
If you take one architectural idea from this post, take this one. Pull-based control beats push-based control the moment customers host things themselves. Your monitoring, your config distribution, and your remote commands should all ride connections the tenant opens.
:::&lt;/p&gt;
&lt;h2&gt;The command mailbox&lt;/h2&gt;
&lt;p&gt;Control rides the channel that&apos;s already open, sixty times an hour.&lt;/p&gt;
&lt;p&gt;A command is a row in a per-tenant queue, nothing more. When a heartbeat arrives, the tower attaches any due commands to the response. The tenant executes them, and the next heartbeat carries the result back as an ack. &quot;Push the latest exercise catalog&quot;, &quot;rotate your signing key&quot;, &quot;force a heartbeat now&quot; are all mail in a mailbox, picked up on a schedule the tenant controls.&lt;/p&gt;
&lt;p&gt;Three details keep it honest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every command has a TTL.&lt;/strong&gt; If it isn&apos;t acked within a few beats it expires and surfaces as failed. A command aimed at a tenant that went down shouldn&apos;t sit in the queue forever, and the operator should see that it never landed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handlers are idempotent.&lt;/strong&gt; Delivery is at-least-once on a good day: the tenant can execute a command and then lose the connection before the ack goes out, so the same command can arrive twice. Commands are therefore written against desired state (&quot;you should be on catalog version 12&quot;), never against counts of actions (&quot;apply 12 updates&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Acks carry structured results.&lt;/strong&gt; What version was applied, what failed, how long it took. The tower&apos;s UI shows command history per tenant with timestamps, without anyone grepping logs on a box they can&apos;t reach.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./4-command-mailbox.svg&quot; alt=&quot;Command lifecycle: queued, dispatched on a heartbeat response, acked on the next one, expired if the tenant stays silent&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That history is a side effect of the design, and in healthcare it&apos;s half the value. Auditors like knowing who told which tenant to do what, when, and whether it worked.&lt;/p&gt;
&lt;h2&gt;Trust, but signed&lt;/h2&gt;
&lt;p&gt;Since tenants are the ones talking to the tower, the tower needs to know a tenant is who it claims. Every tenant request is &lt;strong&gt;Ed25519-signed&lt;/strong&gt;: headers carry the tenant code, a signature over a canonical request string, a timestamp, and a single-use nonce. The tower rejects stale timestamps and replays. Revoking a tenant&apos;s key is how you suspend a tenant. There is no shared static API key sitting in every environment, which was the first thing I wanted gone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6-signed-request.svg&quot; alt=&quot;How a signed request is verified: canonical string, signature check, freshness window, single-use nonce&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;One image, any tenant&lt;/h2&gt;
&lt;p&gt;We build &lt;strong&gt;one set of container images&lt;/strong&gt;, versioned by tag. The same image runs for every tenant; anything tenant-specific is runtime configuration injected at deploy time (domains, LINE app ids, feature knobs). A per-tenant image would have been a maintenance nightmare, so there isn&apos;t one.&lt;/p&gt;
&lt;h2&gt;Migrations run N times, so they only go forward&lt;/h2&gt;
&lt;p&gt;Every tenant stack boots the same way: a small migration sidecar runs the database migrations first, then the API starts. With N tenants on N schedules, &quot;roll back the migration&quot; stops being a real strategy, so it isn&apos;t one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deploys are upgrade-only.&lt;/strong&gt; Migrations roll forward, including ones that fix a failed deploy.&lt;/li&gt;
&lt;li&gt;The scary-sounding &quot;rollback&quot; button in the control panel is actually &lt;strong&gt;teardown&lt;/strong&gt;: destroying that job&apos;s resources, deliberately destructive, gated behind suspending the tenant first. Recovery from a bad deploy means rolling forward to a newer tag, not version time travel.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Provisioning is automation or it is death&lt;/h2&gt;
&lt;p&gt;I learned this the hard way. Hand-deploying tenant number three sucks, and tenant number ten is a resignation letter. Our control plane drives the hosting API end to end: create the project, create the database, create the cache, create the isolated network, push the compose definition with the right image tag and env, wire up domains. It&apos;s a resumable, step-by-step job with a timeline you can watch in the UI, because when it fails (it will, somewhere, someday) you need to see exactly which step and why.&lt;/p&gt;
&lt;p&gt;If you can&apos;t get to &quot;new tenant in minutes, by clicking&quot;, silo&apos;s costs stop being worth it. That automation is load-bearing.&lt;/p&gt;
&lt;h2&gt;One provisioner, several deployment routes&lt;/h2&gt;
&lt;p&gt;The tower doesn&apos;t hardcode a single way to make a tenant exist. Every provisioning job runs against a small provisioner interface (create the stack, report status, tear it down), and the deployment type is just data on the tenant record. Two routes exist today, and they look nothing alike under the hood.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Managed.&lt;/strong&gt; The tower drives a cluster API end to end: create the project, the database, the cache, the isolated network, push the compose definition with the right image tag, wire up the domains. Then, before anything boots, the tower injects the callback configuration (tower URL, tenant code, signing key material) as runtime environment. The stack&apos;s first act on startup is to call home. Nobody SSHes anywhere, nobody edits files on a box, and the tenant-to-tower connection is wired by the same automation that wired the tenant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manual.&lt;/strong&gt; For on-prem, customer metal, or a hosting setup with no API worth the name. The tower still owns the bookkeeping: it issues the tenant code and credentials and holds the record, but the stack is deployed by hands (ours or the customer&apos;s) in a network the tower will never see. The stack boots with a bootstrap config, and the first heartbeat doubles as registration: the tenant tells the tower it&apos;s alive, which URLs actually work, what versions it&apos;s running. The tower learns the topology from the tenant, in the only direction that was ever going to work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3-provisioner-routes.svg&quot; alt=&quot;Two deployment routes converging on the same first heartbeat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After that first heartbeat the routes converge completely: same monitoring, same command queue, same stats pipeline. The rest of the tower doesn&apos;t know or care which route a tenant arrived by. Adding a third route (another cluster API, a marketplace install, whatever next year&apos;s procurement demands) means implementing an interface, not redesigning the plane.&lt;/p&gt;
&lt;h2&gt;What&apos;s bad about this control plane&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Everything is at least one heartbeat late.&lt;/strong&gt; A command lands within a minute if the tenant is healthy, and its confirmation arrives a beat later. For &quot;push the catalog&quot; that&apos;s irrelevant. For &quot;something is wrong, act now&quot; it&apos;s an eternity, and there is no faster path, by construction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Silence is ambiguous.&lt;/strong&gt; If a tenant stops heartbeating, it might be down, partitioned, or just backed off after a network blip. You can&apos;t probe it to find out, because you can&apos;t probe it at all. First-line diagnosis is comparing timestamps and hoping; real diagnosis means a human at the hospital.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You are hand-rolling a message queue.&lt;/strong&gt; TTLs, retries, idempotency, acks are the vocabulary of a message broker, and so are the bug classes. The protocol only stays small because commands stay rare and boring. The day someone wants chatty remote control, this design fights them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No interactive debugging.&lt;/strong&gt; No SSH, no log tailing, no port-forward into a tenant. You see exactly what the tenant chooses to send. Debugging an on-prem stack means asking the customer to paste command output into an email.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The manual route is a second code path with worse visibility.&lt;/strong&gt; A tenant somebody deployed by hand can sit on an old image for months (their right, our blindness), and every difference between how the two routes configure a stack is a difference that can bite during an incident.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key rotation is a careful dance.&lt;/strong&gt; The signing key authenticates the very channel you&apos;d use to fix the signing key. Rotation has to overlap old and new keys long enough for the tenant to pick the new one up, or you&apos;ve suspended your own tenant and the recovery is a car ride.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why I did it anyway&lt;/h2&gt;
&lt;p&gt;Because the alternatives lose worse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It works behind anything.&lt;/strong&gt; NAT, hospital firewalls, a basement network with one outbound rule. A push-based control plane needs the customer to open holes toward me; polling needs nothing I don&apos;t already have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Your control plane cannot connect to our network&quot; ends the security review.&lt;/strong&gt; Inbound-zero is a one-line answer that procurement can verify. Nobody has to trust my promises about firewalls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One protocol for every tenant.&lt;/strong&gt; Cloud, on-prem, manual, managed: same heartbeat, same queue, same dashboard. No special tooling for special customers, and that kind of entropy is what kills small teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The scope is bounded on purpose.&lt;/strong&gt; Commands are rare, idempotent, and low-frequency, so the mailbox stays small and boring. Small and boring is exactly what you want in a control channel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Degradation is graceful.&lt;/strong&gt; If the tower is down, tenants back off and retry. Nothing cascades, nothing times out loudly, and when the tower comes back the fleet walks in over the next minute or two.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;The costs, honestly&lt;/h1&gt;
&lt;p&gt;Everything above was the sales pitch. Here&apos;s the bill.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You operate N of everything.&lt;/strong&gt; N databases to migrate, N stacks to patch, N sets of logs. Mitigation is the heartbeat-and-stats pipeline plus disciplined structured logging (single-line JSON with an event name, one summary line per request), so &quot;what&apos;s happening across the fleet&quot; is a dashboard, not an SSH loop. But the fleet reality doesn&apos;t go away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every tenant has a floor cost.&lt;/strong&gt; A database, a cache, storage, app containers each. Our per-tenant footprint is modest (this isn&apos;t Kubernetes-per-customer), but it&apos;s nonzero, and it shapes pricing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-tenant queries do not exist.&lt;/strong&gt; There is no &quot;average across all patients&quot; query, because there is no place where that data lives together. That&apos;s a privacy feature until product asks for a benchmark report, and then it&apos;s an engineering project. The answer is tenants pushing aggregates (which we already do for stats), never a shared warehouse of raw rows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More moving parts to test.&lt;/strong&gt; Signing, replay protection, &quot;the tower never probes tenants&quot;, migration idempotency are invariants now, and invariants need tests or they slowly rot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The control plane is now a product.&lt;/strong&gt; Monitoring UI, provisioning jobs, commands, audit trail. That&apos;s real time that pooled-SaaS competitors spend on features. You&apos;re buying isolation and on-prem capability with your engineering hours; make sure that&apos;s the trade you want.&lt;/p&gt;
&lt;h1&gt;When you should not pick silo&lt;/h1&gt;
&lt;p&gt;To be clear about the flip side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumer or prosumer product, self-serve, price-sensitive: pooled. Silo&apos;s floor cost per tenant will eat your margins.&lt;/li&gt;
&lt;li&gt;Your team is one or two people with no infra automation appetite: pooled or bridge until that changes.&lt;/li&gt;
&lt;li&gt;You will realistically never deploy on customer infrastructure and your buyers don&apos;t ask about data isolation: you&apos;d be paying for paranoia you can&apos;t sell back.&lt;/li&gt;
&lt;li&gt;You need heavy cross-tenant analytics on raw data as a core feature: shared data is your product, and silo fights it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;Pick the cheapest tenancy model your worst day can survive. Write the leak headline, the outage headline, the &quot;restore one customer&quot; runbook, and read them back to yourself. If your buyers are hospitals, geezers with signing authority who ask where data lives before asking what the product does, the cheapest model that survives those questions is a silo, and the only way to afford it is to automate the stamping.&lt;/p&gt;
&lt;p&gt;The design that made it work was three boring rules: one image for every tenant, tenants call home (never the reverse), and migrations only move forward. Everything else was making those rules convenient to live with.&lt;/p&gt;
</content:encoded></item><item><title>How to Run a Fabric Minecraft Server with Docker - Zero to Hero Guide</title><link>https://blog.hrnph.dev/posts/how-to-run-fabric-mc-server-with-docker/</link><guid isPermaLink="true">https://blog.hrnph.dev/posts/how-to-run-fabric-mc-server-with-docker/</guid><description>A beginner-friendly, step-by-step guide to setting up a Fabric Minecraft server using Docker.</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;This is a &quot;Zero-to-Hero&quot; guide designed for someone who has never touched a server, doesn&apos;t know what a terminal is, and just wants to play Minecraft with friends using the Fabric mod loader.&lt;/p&gt;
&lt;p&gt;We are using &lt;strong&gt;Docker&lt;/strong&gt;. Think of Docker as a &quot;robot&quot; that runs software for you in a neat, isolated box so it doesn&apos;t mess up your actual computer.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 1: Get the Tools&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Before we do anything, you need the engine that runs the server.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download Docker Desktop:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot;&gt;docker.com/products/docker-desktop&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download it for your OS (Windows or Mac).&lt;/li&gt;
&lt;li&gt;Install it just like any other program (Next, Next, Finish).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crucial Step:&lt;/strong&gt; Once installed, &lt;strong&gt;OPEN Docker Desktop&lt;/strong&gt;. It must be running in the background. If it asks you to log in, you can usually skip it or create a free account.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Open your &quot;Command Center&quot; (Terminal):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Press the &lt;code&gt;Windows Key&lt;/code&gt;, type &lt;code&gt;PowerShell&lt;/code&gt;, and hit Enter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mac:&lt;/strong&gt; Press &lt;code&gt;Command + Space&lt;/code&gt;, type &lt;code&gt;Terminal&lt;/code&gt;, and hit Enter.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 2: Create the Server Folder (The &quot;cd&quot; stuff)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You need a specific place on your computer where your server files will live. We will do this using the terminal to get you comfortable with it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type this command&lt;/strong&gt; to create a folder named &lt;code&gt;mc-server&lt;/code&gt; (and press Enter):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir mc-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Note: &lt;code&gt;mkdir&lt;/code&gt; stands for &quot;Make Directory&quot;.)&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type this command&lt;/strong&gt; to &quot;walk inside&quot; that folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd mc-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Note: &lt;code&gt;cd&lt;/code&gt; stands for &quot;Change Directory&quot;. Your terminal prompt should now look different, showing &lt;code&gt;mc-server&lt;/code&gt; at the end.)&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 3: The Blueprint (docker-compose.yml)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We need to give the Docker robot a set of instructions. This is called a &quot;Compose file&quot;. It tells Docker: &quot;I want Minecraft, I want Fabric, and I want it on this port.&quot;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the file:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Type &lt;code&gt;notepad docker-compose.yml&lt;/code&gt; and press Enter. It will ask if you want to create a new file. Click &lt;strong&gt;Yes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mac:&lt;/strong&gt; You can use TextEdit, but ensure it saves as plain text. Or just run &lt;code&gt;touch docker-compose.yml&lt;/code&gt; in the terminal to create it, then open that file with any text editor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Copy and Paste the specific code:&lt;/strong&gt;
Copy the code block below exactly. Don&apos;t change anything yet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  mc:
    image: itzg/minecraft-server
    container_name: my-fabric-server
    tty: true
    stdin_open: true
    ports:
      - &quot;25565:25565&quot;
    environment:
      EULA: &quot;TRUE&quot;
      TYPE: &quot;FABRIC&quot;
      VERSION: &quot;1.20.1&quot;
      MEMORY: &quot;4G&quot;
    volumes:
      - ./data:/data
    restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Understand what you just pasted:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;image: itzg/minecraft-server&lt;/code&gt;: This downloads a pre-made Minecraft server created by a legend named &lt;em&gt;itzg&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TYPE: &quot;FABRIC&quot;&lt;/code&gt;: Tells the server to use Fabric, not normal Minecraft.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VERSION: &quot;1.20.1&quot;&lt;/code&gt;: &lt;strong&gt;Important.&lt;/strong&gt; This dictates the game version. Change this to whatever version you and your friends want to play.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MEMORY: &quot;4G&quot;&lt;/code&gt;: Gives the server 4 Gigabytes of RAM. If your computer is weak, lower this to &quot;2G&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volumes&lt;/code&gt;: This saves your world. It creates a folder called &lt;code&gt;data&lt;/code&gt; in your &lt;code&gt;mc-server&lt;/code&gt; folder so your world doesn&apos;t vanish when you turn the server off.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Save the file&lt;/strong&gt; and close the text editor.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 4: Start the Engine&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Now we tell Docker to read that file and build your server.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Go back to your terminal (make sure you are still inside the &lt;code&gt;mc-server&lt;/code&gt; folder).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run this command:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;up&lt;/code&gt;: Start everything.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;: Detached mode. This means &quot;run in the background, don&apos;t lock up my terminal window.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wait.&lt;/strong&gt; The first time you run this, it will take a while (5-10 mins) because it is downloading Minecraft, Fabric, and Java.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check if it&apos;s working:&lt;/strong&gt;
Type this command to watch the logs (the server &quot;thinking&quot;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose logs -f
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;You will see a lot of text scrolling.&lt;/li&gt;
&lt;li&gt;Wait until you see a line that says: &lt;strong&gt;&lt;code&gt;Done (X.XXs)! For help, type &quot;help&quot;&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Press &lt;code&gt;Ctrl + C&lt;/code&gt;&lt;/strong&gt; to stop watching the logs (this &lt;strong&gt;does not&lt;/strong&gt; stop the server, it just stops the scrolling text).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Congratulations!&lt;/strong&gt; You now have a vanilla Fabric server running.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IP Address:&lt;/strong&gt; &lt;code&gt;localhost&lt;/code&gt; (for you).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP Address:&lt;/strong&gt; Your real IP (for friends—you will likely need to Port Forward your router for friends to join, but that is a separate networking topic).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 5: Installing Mods (The &quot;Asshole Beginner&quot; Friendly Way)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You probably want mods. Here is the safest way to do it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Turn off the server&lt;/strong&gt;
You cannot add mods while the server is running.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Locate the mods folder&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open your regular file explorer (Finder on Mac, File Explorer on Windows).&lt;/li&gt;
&lt;li&gt;Find the &lt;code&gt;mc-server&lt;/code&gt; folder we created in Phase 2.&lt;/li&gt;
&lt;li&gt;Inside, you will see a new folder called &lt;code&gt;data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;data&lt;/code&gt;, you will see a folder called &lt;code&gt;mods&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Download your mods&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to Modrinth or CurseForge.&lt;/li&gt;
&lt;li&gt;Download the &lt;strong&gt;Fabric&lt;/strong&gt; version of the mod.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRITICAL:&lt;/strong&gt; The mod version MUST match your server version (e.g., if server is 1.20.1, mod must be 1.20.1).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRITICAL:&lt;/strong&gt; You must also download the &lt;strong&gt;Fabric API&lt;/strong&gt; mod. Almost all Fabric mods require this. Put it in the folder too.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Drag and Drop&lt;/strong&gt;
Drag the &lt;code&gt;.jar&lt;/code&gt; files you downloaded into that &lt;code&gt;mc-server/data/mods&lt;/code&gt; folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Turn the server back on&lt;/strong&gt;
Go back to your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Watch the logs again (&lt;code&gt;docker compose logs -f&lt;/code&gt;) to make sure the server doesn&apos;t crash. If it crashes, one of your mods is probably the wrong version.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 6: You are the Admin (OP Yourself)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You want to be able to ban people or switch to creative mode. You need to access the server console.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run this command&lt;/strong&gt; to execute the admin tool inside the server:&lt;pre&gt;&lt;code&gt;docker exec -i my-fabric-server rcon-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Your terminal prompt will change to something like &lt;code&gt;&amp;gt;&lt;/code&gt;. You are now talking directly to the Minecraft server.&lt;/li&gt;
&lt;li&gt;Type:&lt;pre&gt;&lt;code&gt;op YourMinecraftUsername
&lt;/code&gt;&lt;/pre&gt;
(Replace &lt;code&gt;YourMinecraftUsername&lt;/code&gt; with your actual in-game name).&lt;/li&gt;
&lt;li&gt;It should say &quot;Made YourMinecraftUsername a server operator&quot;.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;exit&lt;/code&gt; to leave the admin console.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;Phase 7: The &quot;Cheat Sheet&quot; for Daily Use&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Save this list. These are the only commands you need to know.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turn Server ON:&lt;/strong&gt;
&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn Server OFF:&lt;/strong&gt;
&lt;code&gt;docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart Server (after adding new mods):&lt;/strong&gt;
&lt;code&gt;docker compose restart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check Server Status/Logs:&lt;/strong&gt;
&lt;code&gt;docker compose logs -f&lt;/code&gt;
&lt;em&gt;(Press Ctrl+C to exit logs)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access Admin Console:&lt;/strong&gt;
&lt;code&gt;docker exec -i my-fabric-server rcon-cli&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Summary of Common Mistakes&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client/Server Mismatch:&lt;/strong&gt; You need to install Fabric and the &lt;strong&gt;exact same mods&lt;/strong&gt; on your own computer&apos;s Minecraft Launcher to join the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrong Java Version:&lt;/strong&gt; If you change the Minecraft version to an old one (like 1.12), the server might fail. The &lt;code&gt;itzg&lt;/code&gt; image usually handles this automatically, but stick to versions 1.18+ for the smoothest experience with this guide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EULA:&lt;/strong&gt; If you edited the file and didn&apos;t put &lt;code&gt;EULA: &quot;TRUE&quot;&lt;/code&gt;, the server will immediately shut down.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Isolated Kubernetes Cluster on Proxmox with Terraform and Ansible - part 1</title><link>https://blog.hrnph.dev/posts/k8s-ansible-terraform-proxmox-tutorial/</link><guid isPermaLink="true">https://blog.hrnph.dev/posts/k8s-ansible-terraform-proxmox-tutorial/</guid><description>Step-by-step guide to building a fully self-hosted, isolated Kubernetes cluster using Proxmox, Terraform, Ansible, MicroK8s packed with Rancher, and ArgoCD accessible via local domain properly - without wasting your home network IPs.</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;In this blog, I show you how to built a &lt;strong&gt;self-hosted Kubernetes cluster&lt;/strong&gt; on &lt;strong&gt;Proxmox&lt;/strong&gt; using &lt;strong&gt;Terraform&lt;/strong&gt;, &lt;strong&gt;Ansible&lt;/strong&gt;, &lt;strong&gt;MicroK8s&lt;/strong&gt;, &lt;strong&gt;Rancher&lt;/strong&gt;, and &lt;strong&gt;ArgoCD&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;hrnph/Proxmox-k8s-Terraform&quot;}&lt;/p&gt;
&lt;h2&gt;What you will get&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A fully functional Kubernetes cluster with Rancher and ArgoCD&lt;/li&gt;
&lt;li&gt;A self-hosted solution with no external dependencies&lt;/li&gt;
&lt;li&gt;An easy to modify and extend templates&lt;/li&gt;
&lt;li&gt;A clear? understanding of how to set up a Kubernetes cluster on Proxmox&lt;/li&gt;
&lt;li&gt;A clear? understanding of how to use Terraform and Ansible for infrastructure automation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
I did this project for my r/homelab, so I don&apos;t have a lot of resources like r/homedatacenter.
with a single Proxmox node with 32GB of RAM and 12 CPU cores.
I love HP Prodesk for this job and you should too for a low budget homelab.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Overview Diagram&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;./0-flow.svg&quot; alt=&quot;Diagram&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Design Decision &amp;amp; Planning of what we want to achieve&lt;/h2&gt;
&lt;p&gt;Before building the cluster, the following goals were set&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Complete network isolation:&lt;/strong&gt; The Kubernetes cluster should not pollute or consume home LAN IP addresses unnecessarily.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Controlled external access:&lt;/strong&gt; Only intentional, limited ingress into the cluster (e.g., Rancher/ArgoCD UIs) should be exposed.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Infrastructure as Code (IaC):&lt;/strong&gt; Both VM creation (Terraform) and configuration (Ansible) must be fully automated and reproducible.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Lightweight Kubernetes:&lt;/strong&gt; Using MicroK8s for a minimal, fast, HA-capable cluster.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Modern management UIs:&lt;/strong&gt; Deploy Rancher and ArgoCD for visual cluster and app control.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;SSH Gateway (Jumpbox):&lt;/strong&gt; Centralized, secured SSH access via a single VM, without needing to expose cluster nodes individually and we can turn this off without affecting the cluster, like if you don&apos;t need local dns access via to master-nodes anymore ex using tailscale or cloudflare tunnels/warps or whatever you want to use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key Components and Flow&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Proxmox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hypervisor that hosts all VMs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terraform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automates VM provisioning inside Proxmox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cluster nodes (Masters, Workers, Jumpbox) created via Terraform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jumpbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure SSH gateway into the cluster private network.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ansible&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automates installation and configuration of MicroK8s, Rancher, and ArgoCD.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MicroK8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes cluster running across master and worker VMs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ingress Controller&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Routes traffic internally from user DNS requests to services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rancher &amp;amp; ArgoCD UIs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Management UIs for the cluster and GitOps applications.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DNS (Pi-hole or /etc/hosts)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local resolution of friendly service names to the exposed master node IP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Tunnels&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure external access to services without exposing IPs, All still defined in Terraform. (for now you can actually just use cloudflare tunnels on jumpbox or master-1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Security Choices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;master-1&lt;/code&gt; exposes a LAN IP (192.168.1.250) for ingress traffic.&lt;/li&gt;
&lt;li&gt;No internal cluster node can initiate outbound connections into the home network.&lt;/li&gt;
&lt;li&gt;Jumpbox can be destroyed after provisioning for improved security hardening.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why MicroK8s?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fast deployment and updates via Snap packages&lt;/li&gt;
&lt;li&gt;Lightweight but HA-capable Kubernetes&lt;/li&gt;
&lt;li&gt;Built-in Helm 3, Ingress, and Cert-Manager&lt;/li&gt;
&lt;li&gt;No etcd dependency (uses Dqlite HA by default)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
This design replicates cloud provider Private Subnets inside a homelab environment.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Step 0: Prerequisites&lt;/h1&gt;
&lt;p&gt;Clone my template repository&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;hrnph/Proxmox-k8s-Terraform&quot;}&lt;/p&gt;
&lt;h1&gt;Step 1: Proxmox Networking&lt;/h1&gt;
&lt;p&gt;Create &lt;code&gt;vmbr0&lt;/code&gt; (LAN) and &lt;code&gt;vmbr1&lt;/code&gt; (Cluster):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/network/interfaces

auto vmbr0
iface vmbr0 inet static
    address 192.168.1.100/24
    gateway 192.168.1.1
    bridge_ports eno1
    bridge_stp off
    bridge_fd 0

auto vmbr1
iface vmbr1 inet static
    address 192.168.3.1/24
    bridge_ports none
    bridge_stp off
    bridge_fd 0
    post-up echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward
    post-up iptables -t nat -A POSTROUTING -s 192.168.3.0/24 -o vmbr0 -j MASQUERADE
    post-down iptables -t nat -D POSTROUTING -s 192.168.3.0/24 -o vmbr0 -j MASQUERADE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Cluster nodes have isolated internet access.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Step 2: Terraform VM Provisioning&lt;/h1&gt;
&lt;p&gt;Prepare your terraform.tfvars file with your Proxmox login and IPs.&lt;/p&gt;
&lt;p&gt;:::tip
2 Master 3 Workers Setup (should go to 3 for Acutal HA, but whatever, this is a good start)
:::&lt;/p&gt;
&lt;p&gt;:::important
You need to also modified the inventory.ini file if you modified the cluster_vms
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm_password    = &quot;your-password&quot;
template_id    = &quot;your-cloudinit-template-id&quot;
target_node    = &quot;your-proxmox-node&quot;
ssh_public_key = &quot;your-ssh-public-key&quot;

cluster_vms = [
  { name = &quot;master-1&quot;, ip = &quot;192.168.3.11&quot;, cores = 2, memory = 4096, disk = 32 },
  { name = &quot;master-2&quot;, ip = &quot;192.168.3.12&quot;, cores = 2, memory = 4096, disk = 32 },
  { name = &quot;worker-1&quot;, ip = &quot;192.168.3.21&quot;, cores = 2, memory = 4096, disk = 32 },
  { name = &quot;worker-2&quot;, ip = &quot;192.168.3.22&quot;, cores = 2, memory = 4096, disk = 32 },
  { name = &quot;worker-3&quot;, ip = &quot;192.168.3.23&quot;, cores = 2, memory = 4096, disk = 32 },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;terraform init
terraform plan -out ./plans/1-init
terraform apply ./plans/1-init
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What is happening&lt;/h2&gt;
&lt;p&gt;Terraform creates VMs in Proxmox using the specified template and cloud-init configuration.
By using the &lt;code&gt;cloud-init&lt;/code&gt; template, it sets up the VMs with the specified IPs and resources.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2 Masters&lt;/li&gt;
&lt;li&gt;3 Workers&lt;/li&gt;
&lt;li&gt;1 Jumpbox (for SSH)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Step 3: Ansible Cluster Configuration&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook -i inventory.ini playbook.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What is happening&lt;/h2&gt;
&lt;p&gt;Ansible configures the VMs by accessing the VMs via SSH through the jumpbox.
Each node and also te jumpy is configured in the inventory.ini file that ansible use.
Ansible then ssh into the jumpbox and then into the master-1 node to install microk8s.
and continues to install microk8s on the other nodes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install MicroK8s&lt;/li&gt;
&lt;li&gt;HA cluster setup (Join the cluster to the cluster)&lt;/li&gt;
&lt;li&gt;Rancher + ArgoCD deployment&lt;/li&gt;
&lt;li&gt;Expose via Ingress&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Step 4: Access Rancher and ArgoCD&lt;/h1&gt;
&lt;p&gt;DNS Mapping:
This can be done in your router or &lt;code&gt;/etc/hosts&lt;/code&gt; file.
by adding the following lines to your &lt;code&gt;/etc/hosts&lt;/code&gt; file for linux and macOS&lt;/p&gt;
&lt;p&gt;:::note
help yourself if you are using Windows, not my forté
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.1.250 rancher.madoka
192.168.1.250 argocd.madoka
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then access:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rancher: &lt;code&gt;https://rancher.madoka&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ArgoCD: &lt;code&gt;https://argocd.madoka&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Full UI access secured.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;📂 Important File Overview&lt;/h1&gt;
&lt;p&gt;This section explains the key files in this repository, what they do, and why they matter.&lt;br /&gt;
&lt;strong&gt;If you&apos;re new to Terraform or Ansible&lt;/strong&gt;, read this carefully before editing!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File / Folder&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the infrastructure Terraform will create: VMs for Kubernetes masters, workers, and the jumpbox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform.tfvars.example&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A sample file showing what variables you need to define for Terraform (Proxmox login, cluster IPs, VM specs). Copy to &lt;code&gt;terraform.tfvars&lt;/code&gt; and edit your values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform.tfvars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your actual Terraform variable configuration. Sensitive information like Proxmox API password, SSH public key, cluster VM details are placed here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inventory.ini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ansible inventory file. Lists all VM IPs and configures SSH access via the jumpbox. Must match the IPs used in &lt;code&gt;terraform.tfvars&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;playbook.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Main Ansible playbook. Automates the setup of MicroK8s, Rancher, and ArgoCD across the cluster. Fully sequential and clean.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/plans/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Terraform plans (output from &lt;code&gt;terraform plan -out&lt;/code&gt;). Used to pre-approve and manage infrastructure changes safely.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Terraform working directory (created by &lt;code&gt;terraform init&lt;/code&gt;). Contains plugins, backend data. No need to touch manually.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform.tfstate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Terraform state file. Tracks everything Terraform created. &lt;strong&gt;Do not edit manually.&lt;/strong&gt; Keep it safe to manage future changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/roles/&lt;/code&gt; (optional)&lt;/td&gt;
&lt;td&gt;Empty or deprecated for now. Originally for modular Ansible roles but merged into &lt;code&gt;playbook.yml&lt;/code&gt; for simplicity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full documentation of the project, setup steps, file explanations, and additional tips for modification.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h1&gt;Final Thoughts&lt;/h1&gt;
&lt;p&gt;✅ Secure Kubernetes cluster&lt;br /&gt;
✅ Rancher and ArgoCD fully operational&lt;br /&gt;
✅ Infrastructure as code&lt;br /&gt;
✅ Minimal home network exposure&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;:::tip
Next upgrade will be adding Cloudflare Tunnels for public network access.
This is a great way to expose services without exposing your home network IPs.&lt;/p&gt;
&lt;p&gt;for now you can actually just use cloudflare tunnels on jumpbox or master-1, This is also applied to tailscale or whatever you want to use
:::&lt;/p&gt;
</content:encoded></item></channel></rss>