It’s a Tuesday and you’re three hours into an ingress controller upgrade. The CRD schema changed, the annotations you copied from a blog post two years ago are deprecated, and cert-manager wants a newer API version than the one your Helm chart pins.
The application behind all of this is one Go binary and a Postgres database. It serves a few hundred requests a minute. Nobody has touched its code in a fortnight.
That is the moment worth noticing: when Kubernetes is overkill, the tell is not that anything is broken. Everything works. It is that the cluster has quietly become the thing you operate, and the product has become a tenant.
What it actually buys you
Worth being fair, because the case for Kubernetes is real and I reach for it regularly.
It gives you bin-packing across many workloads, so a fleet of services shares machines efficiently. It gives you a declarative API that a platform team can build on, so twelve product teams deploy without twelve bespoke pipelines. It gives you self-healing, rolling updates, horizontal autoscaling and namespace-level isolation as defaults rather than as things you script. And it gives you a portable vocabulary: an engineer who knows Kubernetes knows most of your infrastructure on day one.
Every one of those benefits scales with the number of services and the number of teams. None of them scale with how modern you want to feel.
The bill nobody itemises
The cost is not the control plane fee. It is the surface area you have agreed to keep current.
Kubernetes ships three minor releases a year and supports each for about fourteen months, and you can skip at most one minor version when upgrading. So there is a permanent, non-negotiable upgrade cadence: roughly every four months something needs planning, testing and a maintenance window, forever. Fall behind and you are doing multi-hop upgrades on an unsupported version, which is the worst combination available.
Underneath that sit the components nobody counts when they say “we’ll just run Kubernetes”: a CNI plugin, a CSI driver, an ingress controller, cert-manager, external-dns, a metrics pipeline, an autoscaler, RBAC, and whatever operators your databases need. Each has its own release cycle, its own breaking changes and its own opinions about the others. That is where the Tuesday goes.
Then there is debugging. A request that used to fail in one place now fails in one of several, and telling apart an application bug from a NetworkPolicy, a readiness probe, a DNS caching issue or a node under memory pressure is a genuine skill. On a good team that skill exists. On a three-person team it exists in one person’s head and goes on holiday with them.
Signals you don’t need it yet
- You have fewer than about five services, and most of them are one binary and a database.
- One team owns everything, so there is nothing to isolate from anyone.
- Traffic is predictable enough that you size for peak and stop thinking about it.
- Nobody on the team has run a cluster in production before.
- Your deploys are already fine. Nobody is complaining about them.
- You cannot name the specific problem Kubernetes is solving, only the general one.
- The cluster would run on two nodes, which means you have a scheduler for a decision with two possible answers.
Signals you probably do
- Multiple teams deploying independently and tripping over each other’s environments.
- Enough services that per-service deployment scripts have become their own maintenance problem.
- Genuinely variable load where autoscaling saves real money, not hypothetical money.
- A compliance or tenancy requirement that maps cleanly onto namespaces and network policy.
- You are already running an operator-based product, like a database platform, that expects Kubernetes.
- Somebody’s whole job is the platform, and it is not also somebody’s whole job to ship features.
That last one is the honest test. Kubernetes is not expensive to install. It is expensive to own, and ownership needs a name attached to it.
What I’d reach for instead
Roughly in order of how much you take on.
One server, systemd, and a reverse proxy. Underrated to the point of being contrarian, and completely adequate for a large number of real businesses. A VPS from Hetzner, InterServer or DigitalOcean, nginx or Caddy in front, systemd units for your services, automated backups off the box. Restart policies and health checks are built in; you are not going without them, you are just getting them from init instead of from a control loop.
Docker Compose on one box. The step up when you want containers without a scheduler. The entire deployment is a file you can read in one screen:
services:
app:
image: ghcr.io/acme/api:1.4.2 # pinned, deployed by changing this line
restart: always
ports: ["127.0.0.1:8080:8080"] # proxy handles TLS and the public port
env_file: .env
depends_on: [db]
db:
image: postgres:16 # pin the major, upgrade deliberately
restart: always
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:
The honest limitation: one box is one box. Losing it is an outage, and a redeploy is a brief gap unless you put something in front. For plenty of internal tools and early products, that is an acceptable trade stated out loud rather than an oversight.
A platform that runs containers for you. Cloud Run, App Runner, Fly.io, Render, Railway. You hand over a container image and get scaling, TLS, rolling deploys and health checks without operating any of it. This is the option most teams should look at hardest, because it buys back the majority of what Kubernetes offers at almost none of the operational cost. You pay in per-unit pricing and in reduced control, and both are usually worth it below a certain scale.
ECS on Fargate, or Nomad. Real schedulers with a fraction of the concepts. If you need orchestration but not the ecosystem, these are legitimate destinations rather than waypoints on the road to Kubernetes.
And if you have decided Kubernetes genuinely is the answer, use a managed control plane, and consider a lightweight distribution such as k3s for small or edge deployments. Do not build the cluster yourself to save money. That is the most expensive saving in infrastructure.
Arguments that don’t survive contact
“We’ll need it eventually.” Possibly. Adopting it three years early costs three years of upgrade cycles to buy an option you may not exercise, and the migration is not much harder later than it is now.
“It’s the industry standard.” For orchestrating fleets, yes. Most applications are not fleets. Standard does not mean universally appropriate, and the same argument would put a load balancer in front of a single server.
“It avoids vendor lock-in.” Partly true and frequently oversold. You have swapped a cloud provider’s API for a distribution, a set of controllers, and a pile of YAML that encodes assumptions about all of them. Portability lives in your container images and your data, not in the scheduler.
“It’ll help us hire.” It helps you hire people who want to run Kubernetes. Whether that is the same as the people who will build your product is worth a moment’s thought.
You’re not painting yourself into a corner
This is the part that makes the decision easy to defer, and it is the most useful thing in this post.
The durable investments are containerising your application, externalising configuration into environment variables, keeping state out of the application and in a database or object storage, exposing a health endpoint, and logging to stdout. Do those and your app already satisfies most of what a Kubernetes manifest asks for. The scheduler underneath is genuinely swappable.
So the choice is not Kubernetes now or a painful rewrite later. It is Kubernetes now, or the same containers running somewhere simpler until the complexity is earned.
FAQ
How many services before Kubernetes makes sense?
There is no clean number, and anyone offering one is guessing. The better question is how many teams deploy independently. One team can coordinate without a platform. Four cannot, and that is when the abstraction starts paying for itself.
Is Docker Compose really production-ready?
On a single host, for a workload that tolerates brief restarts, yes. It runs plenty of real systems. What it does not give you is failover across machines, so be clear that losing the host means an outage and decide whether that is acceptable rather than discovering it.
Doesn’t managed Kubernetes remove the operational burden?
It removes the control plane, which is the part that was never your main problem. Ingress, storage, networking, upgrades and the add-on ecosystem all remain yours. Managed offerings genuinely help, and they narrow the gap rather than closing it.
What about autoscaling? I can’t get that elsewhere.
You can. Cloud Run, App Runner, Fly and ECS all autoscale, several of them to zero, with no cluster to maintain. Autoscaling is a common reason to reach for Kubernetes and one of the weakest, because it is the feature most thoroughly commoditised elsewhere.
How hard is it to migrate later?
Much easier than migrating off it. If your app is containerised, stateless, configured by environment and observable, moving to Kubernetes is mostly writing manifests. The hard parts of that migration are the ones you should be fixing anyway.
The one thing to remember
Kubernetes is not complicated by accident. It is complicated because it solves a genuinely complicated problem, and if you do not have that problem you are paying the complexity and collecting none of the benefit.
So make it a decision with a reason attached. Write down the specific thing you cannot do today, and check whether a cluster is the cheapest way to fix it. Sometimes it is. Often the honest answer is one server, a reverse proxy, and an afternoon spent on backups instead.
Trying to decide?
I get called in on both sides of this: teams drowning in a cluster they did not need, and teams who genuinely need one and are putting it off. Work I take on:
- A straight assessment of whether Kubernetes fits your team and workload, with the reasoning written down rather than an opinion delivered.
- Building the simpler thing properly: single-host or Compose deployments with TLS, health checks, deploys and backups that hold up in production.
- Moving off an over-provisioned cluster onto something proportionate, without downtime.
- Getting an application genuinely portable so the decision stays reversible: containerised, stateless, configured by environment, observable.
- Setting up managed Kubernetes properly when it is the right answer, including the upgrade process nobody plans for.
- Reviewing the cost, in money and in engineering hours, of what you currently run.
Tell me how many services you run and how many people deploy them, and I will tell you what I would build.