Why orchestration
You can run one container with docker run. But production means many containers, across many machines, that must survive crashes, reboots, scaling, and rollouts — without you babysitting them. This lesson is the honest bridge from a single docker run to a cluster: the exact problems orchestration solves, the anatomy of a Kubernetes cluster, why a Pod is not the same thing as a container, and — just as important — when not to reach for Kubernetes at all.
docker run stop being enough. This lesson is concepts and reading; nothing here needs a cluster yet. From K2 onward the labs apply real YAML to a real cluster, so install minikube or kind locally now, or plan to use EKS in K5. Verify versions and API fields against the current Kubernetes docs — the project moves fast.Learning objectives
- Name the concrete production problems
docker runleaves unsolved and that orchestration solves. - Describe the anatomy of a cluster: control plane (api-server, etcd, scheduler, controller-manager) and nodes (kubelet, container runtime, kube-proxy).
- Explain the declarative model — you describe desired state, controllers reconcile toward it.
- Distinguish a Pod from a container and say why the Pod is the smallest unit k8s schedules.
- Decide honestly when not to use Kubernetes — the cases where it is the wrong tool.
1 · What `docker run` cannot do
Here's a night you might recognize. You shipped a container to a VM with docker run -d. It worked. Everyone was happy. Then at 3:17am the app hit an out-of-memory error and the process died — and because nothing was watching, it stayed dead until a customer emailed at 8am. You SSH in, run docker run again, and go back to bed. Two weeks later the whole VM reboots for a kernel patch and your container doesn't come back at all, because you never set it to start on boot. That drip of tiny 3am chores — restart it, move it, scale it, put it behind a load balancer — is the problem orchestration exists to erase.
Here's the mental shift. docker run is a one-shot command: it starts a container and then walks away. Nobody is left minding the store. What production actually wants is a standing promise — "keep this thing running, in this shape, no matter what breaks" — and a command that runs once can't make a promise that lasts forever. Think of the difference between lighting a candle and installing a thermostat. The candle (docker run) gives you light right now, but if a draft blows it out, it's out. The thermostat (an orchestrator) has a target and a loop that keeps checking reality against it — furnace kicks on when the room drifts cold, off when it's warm — with no one in the room. Orchestration is a thermostat for your containers.
The common beginner mistake here is assuming Docker's own flags are "close enough." --restart=always does bring a crashed container back — but only on the same host, and only if the Docker daemon itself is alive. The instant the machine dies, that flag dies with it, and everything it was restarting is simply gone. Docker gives you the pieces for one container on one box; orchestration gives you a system that spans many boxes and outlives any one of them. Read the table below as a list of promises you'd otherwise have to keep by hand, at 3am, forever.
A single docker run is perfect for one container on one machine. Production is not that. The moment you have real traffic, you need answers to questions Docker alone does not answer: what restarts a crashed container at 3am? What moves it when its host dies? How do ten replicas share one stable address? How do you roll out v2 without dropping requests, and roll back in seconds when it's bad? Orchestration is the standing machinery that answers all of these automatically.
docker run on one host: when the container dies nothing brings it back, and a reboot loses everything. On the right, the orchestrator holds a target of 3 replicas and reschedules onto a healthy node the instant one dies.
Compare the two halves split by the dashed line: the fragile single host on the left, the self-healing fleet on the right.
- The left panel is one VM running one container with
docker run. The red box is the crashed container; the red arrow points to "nobody watching" — the command already finished, so the container stays dead and a host reboot wipes it entirely. - The right panel starts from a purple desired state box ("3 replicas") that fans out to three nodes. Green boxes are healthy replicas; node 3's replica has failed (red).
- The payoff is the bottom-right arrow: the orchestrator notices the gap and reschedules the lost replica onto a healthy node, so the count returns to 3 with no human involved.
In short: docker run gives you a container until something breaks; an orchestrator gives you a count of running replicas it defends across the whole fleet.
| Production need | With bare `docker run` | With an orchestrator |
|---|---|---|
| Restart on crash | --restart flag, per host only | Self-healing across the whole cluster |
| Host dies | Containers on it are gone | Rescheduled onto healthy nodes |
| Scale to N replicas | Run N commands, track them yourself | replicas: N — one field |
| Stable address for N replicas | Wire up your own load balancer | A Service gives one virtual IP + DNS |
| Zero-downtime deploy | Manual, error-prone | Rolling update built in, with rollback |
| Config & secrets | Bake in or pass by hand | ConfigMaps / Secrets, mounted at runtime |
2 · The declarative model — desired vs actual state
Think about how you get a haircut. You don't stand behind the barber narrating every scissor stroke — "now move up two millimeters, now tilt the blade, now snip." You describe the result you want ("short on the sides, leave the top") and trust them to keep cutting until reality matches that description. If you come back in three weeks and it's grown out, you don't re-explain the whole procedure — you just say "same as last time," and they close the gap again. That is the entire philosophy of Kubernetes in one everyday act: you describe the end state, not the steps, and something else does the work of getting there and staying there.
This matters because it's the opposite of how most of us first learn to automate. The instinct is to write a script: "do this, then this, then this." That's imperative — a recipe of steps — and it has a fatal flaw in a world where things break: a script runs once, top to bottom, and then it's over. The moment reality drifts (a container dies, a node reboots), the script has no idea and no reason to care; it already finished. The declarative model flips this. You hand the cluster a description of the world you want — "3 replicas of this image on port 80" — and a controller compares that description to what's actually running, over and over, forever, taking action every time they disagree. You stop writing how and start writing what.
The common trap for newcomers is treating a Kubernetes manifest like a shell script — imagining kubectl apply "runs" the YAML the way bash runs commands. It doesn't. Applying a manifest doesn't execute anything; it records a wish in the cluster's database, and independent controllers then race to make that wish true. That's why you can apply the same file ten times and nothing bad happens — you're re-stating the same desired state, not re-running ten actions. Hold onto the phrase desired vs actual; every self-healing behavior in the rest of this track is just that gap being closed automatically.
This is the single most important idea in Kubernetes, and it's what separates it from a shell script. You do not tell the cluster "start this container, then that one." You declare the desired state — "I want 3 replicas of this image exposed on port 80" — and write it down as YAML. A set of controllers continuously compares desired state to actual state and takes whatever action closes the gap. Delete a Pod and the controller notices actual < desired and makes a new one. This loop is called reconciliation.
Read it left-to-right as a one-way pipeline, following the four arrows from what you do to what the cluster ends up doing.
- The first box, "You write YAML," is the only human step — you describe the state you want, you don't issue commands.
- The middle two boxes are the machinery: the purple api-server stores that desired state, then the amber controller reconciles — repeatedly comparing what's wanted against what's real.
- The payoff is the green end box, "Actual state matches": the arrows all flow toward it because the controller keeps acting until reality equals the recorded wish.
In short: you record a wish and the api-server plus controllers do the work of making — and keeping — actual state equal to your declared desired state.
3 · Cluster anatomy — control plane + nodes
Picture a busy restaurant. Out front there's a manager taking orders, writing them on tickets, and deciding which cook handles what; in back there's a line of cooks who actually make the food. You, the customer, never walk into the kitchen and start frying — you talk to the front, and the front drives the back. A Kubernetes cluster is organized exactly like this. The control plane is the front-of-house brain that takes your orders and decides what runs where; the worker nodes are the kitchen line that does the actual cooking. When people say "I can't reach the cluster," nine times out of ten they mean they can't reach the manager out front — the control plane — not that the cooks stopped working.
Understanding this split is what turns cluster debugging from guesswork into a checklist. Every component has one job, and when something misbehaves, knowing whose job it was tells you where to look. The manager (api-server) is the only one allowed to write in the reservation book (etcd) — so if two people try to change the same thing at once, the book stays consistent. The seating planner (scheduler) decides which table (node) a new party (Pod) goes to. The floor supervisors (controller-manager) keep checking that every table that should be occupied actually is, and re-seat parties when a table breaks. And on each table sits a waiter (kubelet) who takes instructions from the front and reports back whether the food actually arrived.
The mistake beginners make is picturing the control plane as a single magic box, so when something breaks they have no idea which piece failed or what the blast radius is. It's worth internalizing early that these are separate processes with separate failure modes — a fact Exercise 5 makes you reason about directly. If etcd (the reservation book) is lost, the cluster forgets everything it was supposed to be doing; if the scheduler stops, existing tables keep serving but no new parties get seated; if a single node's kubelet dies, only that one table goes quiet while the rest of the restaurant runs fine. You talk only to the control plane (via kubectl); it drives the nodes — so trace the diagram below left-to-right as the path your order actually takes.
A cluster is two kinds of machines. The control plane is the brain that decides what should run where; the worker nodes are the muscle that actually runs your containers. You talk only to the control plane (via kubectl); it drives the nodes.
Trace it strictly left-to-right along the arrows — this is the path a single request takes from your terminal into the cluster.
- It starts at the blue kubectl / you box on the far left: you never touch the nodes directly, you only speak to the control plane.
- The arrows then run through the control-plane pieces in order — the purple api-server (the one front door), which persists to etcd, and the scheduler + controllers that decide what runs where.
- The payoff is the green box on the right, kubelet on node: the last hop, where the control plane's decision is handed to the node agent that actually starts the container.
In short: you talk only to the api-server, and its decisions flow down the chain to the kubelet that runs your work on a node.
| Component | Lives on | What it does |
|---|---|---|
| kube-apiserver | Control plane | The front door. Every read/write goes through it; it validates and persists. |
| etcd | Control plane | The cluster's database — the single source of truth for all state. |
| kube-scheduler | Control plane | Decides which node an unscheduled Pod should run on (fit, resources, constraints). |
| controller-manager | Control plane | Runs the reconciliation loops (Deployment, ReplicaSet, node controllers…). |
| kubelet | Every node | The node agent — starts/stops Pods and reports health back to the api-server. |
| container runtime | Every node | Actually runs containers (containerd/CRI-O). |
| kube-proxy | Every node | Programs the node's networking so Service virtual IPs route to Pods. |
On a managed service like EKS (K5), AWS runs and secures the entire control plane for you — you only manage nodes. That's the main reason most teams use managed Kubernetes rather than running etcd themselves.
4 · Pods vs containers
You spend the whole containers track learning about containers — and then Kubernetes hands you a word you've never seen: Pod. Almost everyone's first reaction is quiet confusion: "Wait, I thought I was deploying containers. What's a Pod, and why do I suddenly have to care?" It feels like an unnecessary layer of jargon between you and the thing you already understand. It isn't — but the friction is real, so let's name it plainly and make it click before you ever write one.
Here's the analogy that makes it stick: a container is a single roommate, and a Pod is the apartment they live in. Usually one roommate lives alone — one Pod, one container — and the apartment is basically just a wrapper around that one person. But sometimes two roommates truly need to share the same apartment: same address (one IP), same phone line (localhost), same fridge (shared storage), and — crucially — they move in and move out together. That's a Pod with a main container and a sidecar: a log shipper or proxy that only makes sense living right next to the app and dying when it dies. Kubernetes schedules apartments, not individual roommates, because everything that must share an address and a lifecycle has to be placed as one unit.
Why bother with the wrapper at all, when 90% of the time it's just one container? Because the smallest thing the scheduler can place on a node needs a single, stable network identity and a single lifecycle — and "a container plus its inseparable helpers" is the honest unit of deployment, not the lone container. The mistake to avoid is reaching for multi-container Pods too eagerly: cramming your database and your API into one Pod because it's "convenient" is an anti-pattern, because they can no longer scale, restart, or move independently. The rule of thumb — one main container per Pod, add a sidecar only when it must share the app's fate — is worth tattooing on your brain now, before the YAML tempts you otherwise. Read the minimal Pod below with that lens: it's one apartment, one roommate.
Newcomers assume Kubernetes runs containers. It doesn't — directly. The smallest unit it schedules is a Pod: one or more containers that share a network namespace (same IP, same localhost) and can share storage. Usually a Pod is one app container; a second sidecar container (a log shipper, a proxy) is added only when it must live and die with the main one.
localhost, and storage, and live and die together. The scheduler always places the Pod.
Read the two outer boxes as apartments; the arrows at the bottom show what the scheduler actually acts on.
- The left Pod is the 90% case: one green app container inside one Pod. The amber strip underneath is the shared identity — a single IP, one
localhost, and shared storage. - The right Pod holds two green containers, an app and a sidecar (a log shipper or proxy). They sit in the same apartment, so they share that same amber strip and move in and out together.
- The payoff is the purple box both arrows point into: the scheduler places the Pod as one unit onto a node — never a lone container — because everything sharing an address and a lifecycle must be placed together.
In short: A container is a roommate; a Pod is the apartment. Kubernetes schedules apartments, and a sidecar only joins when it must share the app's address and fate.
pod.yamlapiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: app
image: ghcr.io/example/web:1.0.0 # pin a version, never :latest
ports:
- containerPort: 8000
run.shkubectl apply -f pod.yaml # create/update from the manifest
kubectl get pods # is it Running?
kubectl describe pod web # events, image, node, why it's Pending/CrashLooping
kubectl logs web # the container's stdout/stderr
NAME READY STATUS RESTARTS AGE
web 1/1 Running 0 12s
5 · When NOT to use Kubernetes
Here's a failure mode that plays out in real companies every year. A two-person startup reads that "everyone serious runs Kubernetes," spins up a cluster for their single Flask API, and six months later has spent more engineering hours fighting cluster upgrades, networking quirks, and expiring certificates than they ever spent building the actual product. The app was never the problem — the infrastructure they didn't need became a second full-time job neither of them signed up for. Nobody warned them, because it's rarely fashionable to say "you don't need this." So this section says it plainly: Kubernetes is often the wrong first choice, and knowing that is a mark of experience, not ignorance.
The honest framing is that Kubernetes is a power tool with a real running cost, and like any power tool, using it for a job that a screwdriver would do is not impressive — it's a hazard. A cluster doesn't just sit there; someone has to own it. That means patching security holes, running version upgrades that can break things, managing RBAC, watching costs, and being on call for the cluster itself — a layer of pager duty entirely separate from your application. That ownership is the true price of Kubernetes, and it's almost always paid in headcount, not dollars of compute. If no one on the team is that owner, the cluster doesn't stay still — it quietly rots into an unpatched, misconfigured liability, which is genuinely worse than the simple VM you were trying to outgrow.
The most useful instinct to build is to flip the default: don't ask "why not Kubernetes?" — ask "do I actually have the problems from section 1?" Kubernetes earns its complexity only when you're running many services across many machines and you have a team to own the platform. Miss those conditions and a managed host (a PaaS, serverless, or managed containers like ECS/Fargate or Cloud Run) ships the exact same container with a fraction of the operational burden — and, because you containerized anyway, migrating to Kubernetes later is writing manifests, not rewriting the app. That's the quiet superpower: the decision is reversible, so choosing "not yet" costs you almost nothing. The table below is your decision aid — for each situation, notice it fails at least one of the many-services / many-machines / team-to-run-it tests.
Kubernetes is powerful and expensive in complexity. It is the wrong first choice more often than the internet admits. Reach for something simpler when:
Read it top-to-bottom: three yes/no tests feed one gate, and the gate routes you to one of two outcomes.
- The top row is the three questions that decide the call: do you have many services, many machines, and a team to own the cluster?
- The purple gate in the middle is an AND — it only opens if all three answers are yes. The solid green-labelled arrow is that path; the dashed grey "any no" arrow is the far more common one.
- The two outcomes are the payoff: green "Kubernetes fits" (complexity now buys leverage) versus amber "managed host instead" — a PaaS or serverless that runs the exact same container with a fraction of the ops burden.
In short: Don't ask "why not Kubernetes?" — ask the three questions. Unless every answer is yes, a managed host is the better first choice, and migrating later is just writing manifests.
| Situation | Why k8s is overkill | Reach for instead |
|---|---|---|
| A single container, low traffic | You'd run a control plane to babysit one app | A PaaS: Fly.io, Render, App Runner, ECS |
| A small team, no platform engineer | k8s needs someone to own it full-time | Managed containers / serverless |
| Bursty, event-driven, scale-to-zero | Idle nodes cost money | Lambda / Cloud Run / Fargate tasks |
| A static site or simple API | Enormous machinery for little benefit | A CDN / a managed app host |
| You just want to ship this week | The learning curve is real | Whatever gets it live; migrate later |
✓ Checkpoint — you can move on when you can…
- Name three production problems
docker runalone doesn't solve. - Explain the reconciliation loop in one sentence (desired vs actual state).
- Label the control-plane components and say what etcd and the scheduler each do.
- Explain why a Pod — not a container — is the unit Kubernetes schedules.
- Give two concrete situations where you'd deliberately not use Kubernetes.
You delete a Pod that was created directly with kubectl apply -f pod.yaml (a bare Pod, not owned by a Deployment). What happens, and why is this different from deleting a Pod managed by a Deployment?
Show answer
A startup with one Flask API and two engineers asks whether to launch on Kubernetes. What's the honest answer?
Show answer
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Before you learn the YAML, you should be able to defend why the cluster exists at all — the reflex a lead expects from you in a design review.
Your task: Write a short, concrete answer to "we already have docker run on a VM — why add Kubernetes?" grounded in real failure modes, not buzzwords.
Requirements:
- Name at least three production problems bare
docker runleaves unsolved - For each, state what an orchestrator does instead
- End with one honest caveat: a case where k8s would be the wrong call
💡 Hint: Anchor each point to a 3am failure: a crash, a dead host, a bad deploy.
Show solution
The pitch. Bare docker run is fine until production stress arrives, and then it leaves gaps an orchestrator fills automatically:
- Crash at 3am → a bare container needs
--restartand only recovers on that one host; Kubernetes self-heals by rescheduling failed Pods anywhere in the cluster. - Host dies → everything on it is lost; the scheduler reschedules those Pods onto healthy nodes.
- Bad deploy → manual stop/start drops requests; a Deployment does a rolling update with an instant rollback.
- Ten replicas need one address → you'd hand-roll a load balancer; a Service gives a stable virtual IP and DNS name.
The honest caveat: for a single low-traffic app and a two-person team, this is all cost and little benefit — a PaaS or serverless host is the right call. Kubernetes earns its complexity only when you run many services across many machines.
Context: Understanding the request path demystifies every later debugging session.
Your task: Describe, step by step, what happens inside the cluster from the moment you run kubectl apply -f pod.yaml until the container is actually running.
Requirements:
- Follow the request through the api-server and etcd
- Say where the scheduling decision is made
- Say which component actually starts the container on the node
💡 Hint: Every write goes through the api-server; nothing talks to etcd directly except it.
Show solution
The path, component by component:
kubectlsends the manifest to the kube-apiserver (the only front door), which authenticates, validates, and writes the desired state to etcd.- The Pod now exists in etcd but is unscheduled (no node assigned). The kube-scheduler watches for such Pods, picks a node that fits (resources, constraints), and writes that assignment back through the api-server.
- The kubelet on the chosen node sees a Pod bound to it, tells the container runtime (containerd) to pull the image and start the container, then reports status back.
- kube-proxy programs node networking so any Service in front of the Pod can route to it.
The mental model: you write desired state → api-server persists it → controllers and the scheduler reconcile → kubelet makes it real. Every debugging step later (describe, logs, events) is just reading one stage of this pipeline.
Context: The reconciliation loop is the heart of k8s; you should be able to demonstrate it, not just define it.
Your task: Explain what happens when a node running one of a Deployment's 3 replicas is abruptly powered off, tracing the loop back to 3 healthy replicas.
Requirements:
- Identify which controller notices the gap and how
- Explain the role of node health / heartbeats
- State what the end state looks like and roughly how fast
💡 Hint: Two loops interact here: node health detection and the ReplicaSet's replica count.
Show solution
Setup: a Deployment declares replicas: 3; its ReplicaSet has 3 Pods, one per node. A node is powered off hard.
- The dead node's kubelet stops sending heartbeats. The node controller waits a grace period, then marks the node
NotReadyand eventually evicts its Pods (deleting them from desired-state's point of view). - The ReplicaSet controller now sees actual (2 running) < desired (3). It creates a new Pod to close the gap.
- That new Pod is unscheduled, so the scheduler places it on a surviving healthy node; its kubelet starts the container.
- End state: 3 running replicas again, no human involved. Timing depends on the node eviction timeouts (tens of seconds to a few minutes) — verify the current default grace/eviction settings against the Kubernetes docs.
The lesson: self-healing is not magic — it's two reconciliation loops (node health and replica count) each independently pulling actual state back toward desired state.
Context: The expert skill is knowing when NOT to use the tool you're learning.
Your task: Given four representative systems, decide for each whether Kubernetes is justified, and name the alternative when it isn't.
Requirements:
- System A: a marketing static site
- System B: a monthly batch job that runs 20 minutes then exits
- System C: a platform of 15 microservices for 40 engineers
- System D: a bursty webhook handler, idle most of the day
💡 Hint: Score each on: many services? many machines? need self-healing/portability? bursty?
Show solution
System A — static site → no. A CDN or managed static host is cheaper, faster, and needs no cluster. k8s adds nothing.
System B — monthly batch → no (mostly). A scheduled serverless job (Lambda, Cloud Run job, or a Fargate task) matches the run-then-exit shape without paying for idle nodes. Only put it on k8s if a cluster already exists and a CronJob is convenient.
System C — 15 services, 40 engineers → yes. This is exactly k8s's sweet spot: many services across many machines, a shared platform, self-healing, and a team large enough to own the cluster. The complexity now buys real leverage.
System D — bursty webhook, idle → probably no. Scale-to-zero serverless (Lambda / Cloud Run) avoids paying for idle capacity; on k8s you'd carry baseline nodes or add KEDA/knative to fake scale-to-zero. Use k8s only if it's already your platform.
The rule: Kubernetes earns its keep on many services × many machines × a team to run it. Miss those and a simpler host wins.
Context: A platform engineer must be able to sketch the cluster and reason about what breaks when a given piece fails.
Your task: Produce a labeled diagram of a cluster (control plane + 3 nodes) and, for each component, state the blast radius if it fails.
Requirements:
- Include api-server, etcd, scheduler, controller-manager, and per-node kubelet/runtime/proxy
- For each, describe what still works and what breaks if it's down
- Note which components a managed service (EKS) takes off your plate
💡 Hint: Ask for each: if this dies, can running Pods still serve traffic? Can new ones be created?
Show solution
Failure-domain analysis:
- api-server down → no new changes (no apply, no scheduling of new work), but existing Pods keep running and serving traffic since kubelets act on already-persisted state. The cluster is frozen, not dead.
- etcd down/lost → the most serious: it's the source of truth. Without a backup, cluster state is unrecoverable. Running Pods may limp on, but the cluster can't be managed. Back up etcd.
- scheduler down → running Pods fine; new Pods stay
Pending(unassigned) until it returns. - controller-manager down → reconciliation stops: crashed Pods aren't replaced, rollouts stall. Existing steady-state keeps serving.
- one node's kubelet down → that node's Pods are eventually rescheduled elsewhere; the rest of the cluster is unaffected.
Managed value: on EKS, AWS runs and replicates the api-server and etcd across AZs and handles their backups/upgrades — you inherit a resilient control plane and only own the nodes. That offload is the main reason to pay for managed Kubernetes.
Context: Representative scenario: a Series-B company with 6 services and growing traffic asks platform engineering for a recommendation on adopting Kubernetes. Leadership wants a one-pager, not a religious war.
Your task: Write a decision memo that recommends for or against adopting Kubernetes now, with explicit criteria, costs, and a migration path.
Requirements:
- State the decision up front, then the criteria behind it
- Quantify the ongoing cost: who owns the cluster, what breaks if no one does
- Include a 'revisit if' trigger list so the decision isn't permanent
- Name the interim option and how you'd migrate to k8s later without a rewrite
💡 Hint: Frame it as reversible: containerize now (portable), choose the runtime as a separate, revisitable decision.
Show solution
Recommendation: not yet — containerize now, defer the cluster. Ship the 6 services as containers on a managed host (ECS/Fargate or Cloud Run) today; adopt Kubernetes when the triggers below fire. Rationale, in one page:
Criteria we scored. (1) Number of services: 6 — meaningful but not yet fleet-scale. (2) Team: no dedicated platform owner — the decisive factor. (3) Portability need: low today. (4) Self-healing/scaling: a managed host already provides both. On these, k8s's benefits are largely already met, while its costs are not yet justified.
The real cost of adopting now. Kubernetes needs an owner: upgrades, CVE patching, networking, RBAC, cost control, and on-call for the cluster itself. Without a named owner it rots into an unpatched, misconfigured liability — the worst of both worlds. That headcount is the true price, not the compute.
Revisit if any of these become true: we cross ~10–15 services; we hire/allocate a platform engineer; we need multi-cloud or on-prem portability; a managed host's limits (custom networking, GPUs, operators) start blocking us; or spend on managed containers exceeds what a cluster would cost with an owner.
Migration path (why this is reversible). Because every service is already a container with a Dockerfile and externalized config/secrets, moving to Kubernetes later is writing manifests — not a rewrite. We lose nothing by waiting, and we avoid paying the cluster tax before it buys us anything.