From Local-First Infrastructure to Hybrid Kubernetes
This article describes how to migrate a working, locally owned system into a hybrid Kubernetes architecture without surrendering authority to the cloud. The core move is to keep steady-state workloads and truth on hardware you control, while using Kubernetes scheduling, edge traffic weighting, and cloud replicas to absorb volatility and failure. The result is not “cloud-native,” but resilient: boring in the best sense, legible under stress, and sovereign by default.
How to adopt cloud patterns without surrendering ownership
There’s a certain kind of system that doesn’t introduce itself as modern, but earns trust anyway. It runs on machines you own. It has real users. It survives ordinary failure without ceremony. When something goes wrong, you can still tell what happened and why. It didn’t get here by chasing abstractions; it got here by accumulating operational truth.
The challenge such systems face today isn’t obsolescence. It’s pressure. Traffic is spikier than it used to be. Background workloads are heavier and less predictable. Expectations around uptime have hardened, even as teams remain small. The cloud solved this problem for many organizations by absorbing volatility — but it did so by quietly relocating authority.
What follows is how to migrate a working, locally owned stack into a Kubernetes-based hybrid architecture without moving its center of gravity. The cloud enters as overflow and insurance, not as headquarters. The steady state remains local. Most real work stays on hardware you control. The rest goes elsewhere only when conditions force the issue.
This is not a rewrite. It’s a change in how pressure is handled.
The first decision that matters is refusing the wrong topology. Many introductions to Kubernetes imply that there should be “one cluster” spanning everything, regardless of geography. In practice, stretching a single Kubernetes control plane across a wide-area network creates ambiguity where you most need clarity. Latency turns into uncertainty. Partial network failures blur authority. Incidents become harder to reason about precisely because the system can no longer agree on what “up” means.
Instead, the architecture begins with two independent Kubernetes clusters: one on-premises, one in the cloud. They may run the same software and deploy the same applications, but they do not share a control plane. Each cluster can make decisions locally. Each can fail independently. Authority remains legible.
On your own hardware, the goal is a Kubernetes distribution that feels like infrastructure rather than a product. This is where lightweight distributions like k3s or rke2 enter the picture. Both are maintained by Rancher and are designed explicitly for environments where you control the machines. They bundle the Kubernetes components you actually need, avoid cloud-specific assumptions, and can be installed with a single command on a fresh Linux host. You don’t sign up for anything. You download a binary, run an installer, and you get a functioning cluster.
That simplicity matters. When something breaks, it breaks in places you recognize. When you reboot a node, it comes back the way you expect. At the end of installation, nothing means nothing is running yet — and that’s exactly right. If the cluster doesn’t feel boring at this stage, stop and fix that before going further.
Before introducing any applications, you establish how traffic enters the cluster and how encryption is handled. This ordering is deliberate. Everything else depends on having a predictable front door.
An ingress controller is the Kubernetes component that accepts HTTP and HTTPS traffic and routes it to the correct service inside the cluster. Popular choices like NGINX Ingress or Traefik exist because they are well understood and configurable in plain YAML. You install one by applying a published manifest from the project’s documentation — no account required, no vendor lock-in — and suddenly your cluster can accept external requests.
Alongside ingress, you install cert-manager, which automates TLS certificate issuance and renewal using Let’s Encrypt. cert-manager watches your ingress definitions, requests certificates when new hostnames appear, and renews them long before expiration. The important part is consistency: cert-manager behaves the same way on your own hardware as it does in the cloud. Once this is in place, TLS stops being a manual task and becomes background hygiene.
Only after ingress and certificates are calm and predictable do applications enter the picture.
Containerization is often described as modernization, but in migrations like this it’s better understood as clarification. You are forced to be explicit about what already exists. The right unit of migration is not the microservice you wish you had, but the deployment unit you already understand. A PHP application remains a PHP application. Node services stay Node services. Python workers remain workers. If there is a Java service running under Tomcat because it solves a real problem, it comes along unchanged in spirit.
Each of these runtimes becomes a container image, built using a Dockerfile or Podmanfile that simply describes how that service already runs. These images are deployed into Kubernetes as workloads with health checks and resource boundaries. At this stage, Kubernetes is not reshaping your architecture. It is replacing init systems, process supervision, and hand-rolled restart logic with something more consistent.
The hybrid nature of the system emerges not from clever routing, but from scheduling discipline. Kubernetes decides where workloads run based on metadata you provide. By labeling nodes in your on-prem cluster as preferred execution targets and treating cloud nodes as exceptional, you encode your values directly into the scheduler. Core services naturally land on hardware you own. Cloud capacity exists, but it is only used when local resources are exhausted or unhealthy.
This distinction matters because it is mechanical, not cultural. You do not rely on engineers remembering what should run where during an incident. The system already knows.
As soon as two clusters exist, configuration drift becomes the real risk. One cluster slowly becomes “special,” the other becomes “the real one,” and soon nobody is entirely sure how the system is supposed to look. GitOps tools such as Argo CD or Flux exist to prevent this exact failure mode. You define your applications once, in a version-controlled repository, and each cluster continuously reconciles itself against that definition.
There is no signup process here either. These tools are open-source projects you install into your clusters by applying their manifests. Once running, they pull configuration from your Git repository and ensure that what’s running matches what’s declared. Differences between on-prem and cloud are expressed explicitly, usually through small overlay files that adjust storage classes, replica limits, or node affinity. Failover stops being a hero move and becomes a re-application of the same intent under different conditions.
Only after the system has reached this level of calm does a service mesh make sense. A service mesh is infrastructure that sits between services and handles identity, encryption, retries, and observability. Projects like Linkerd and Istio exist because manually securing and instrumenting service-to-service traffic at scale is error-prone. Installing a mesh means installing a control plane into each cluster and allowing sidecar proxies to be injected alongside selected workloads.
The key is restraint. You use the mesh to make trust explicit and traffic behavior observable. You do not use it to hide topology or pretend the clusters are one big happy family. Each cluster runs its own mesh. Cross-cluster communication passes through explicit gateways. Boundaries remain visible.
The most important rule in the entire system lives outside Kubernetes altogether. The decision that roughly eighty percent of traffic should land on-prem and twenty percent may spill into the cloud is not something the cluster should infer. It belongs at the edge, where traffic first enters your world. This can be implemented using weighted DNS records or a global HTTP proxy service. In either case, health checks determine where traffic flows. Under normal conditions, most requests land locally. When local capacity is exceeded or health degrades, weight shifts automatically.
Inside the clusters, nothing special happens. Each ingress believes it is primary. No request needs to know whether it arrived as part of the eighty or the twenty.
State is where hybrid architectures either tell the truth or lie to themselves. Databases do not tolerate ambiguity well. PostgreSQL remains authoritative on-prem, with asynchronous replicas in the cloud maintained by operators such as CloudNativePG or Patroni. These operators are Kubernetes-native systems that automate replication, backups, and promotion. They are installed like everything else: by applying manifests and configuring them declaratively. Failover is possible, but explicit, and carries a known recovery point objective.
Caches such as Redis are treated as disposable acceleration. They may exist independently in each cluster. They are never treated as truth. Files either move to object storage with clear replication semantics — MinIO or Ceph locally, mirrored to cloud storage — or they remain local with explicit asynchronous copy. The rule is simple: if a component cannot survive cloud failover without corrupting reality, it does not get to pretend otherwise.
The cloud earns its place by absorbing volatility, not by hosting identity. Bursty workloads are pushed behind queues — NATS, RabbitMQ, or similar systems — so that on-prem workers handle the steady rhythm while cloud workers appear only when backlog grows. These workers scale elastically and disappear when demand subsides. Users experience continuity. Operators experience silence.
When the migration is complete, nothing feels dramatic. Kubernetes fades into the background, doing the job init systems once did, only more reliably. The cloud behaves like insurance: invaluable during a storm, largely invisible otherwise. Ownership remains local. Failures remain legible. No part of the system requires belief to function.
That is the difference between adopting cloud patterns and surrendering to them.