Skip to content
You don't need Kubernetes. You need a good Dockerfile and a place to run it.
← ← Back to Thinking Cloud

You don't need Kubernetes. You need a good Dockerfile and a place to run it.

Every few months a team of three to five engineers asks us whether they should move to Kubernetes. The application is a web front-end, an API, a couple of workers and a database. It runs on one or two servers, or on a platform-as-a-service that's getting expensive. Someone has read that Kubernetes is how real companies do it, and they'd like to be a real company.

The answer we give, almost every time, is no. Not "not yet". No. And since that answer sounds like laziness or contrarianism, this article is the reasoning behind it, the alternative we'd build instead, what it costs, and the specific conditions under which the answer changes.

What Kubernetes is for

Kubernetes solves scheduling: given a fleet of machines and a set of workloads with resource requirements, place the workloads on the machines, keep them running, replace them when they die, and expose them to each other and to the world. It solves this extremely well, at any scale, for any kind of workload, with an ecosystem for everything you could want.

Notice what the problem statement requires: a fleet of machines. If you don't have a fleet, you don't have the scheduling problem, and Kubernetes becomes a very sophisticated answer to a question you didn't ask. A team with three services doesn't need bin-packing. It needs its containers to run, restart, scale from two to six when there's traffic, and get an HTTPS URL.

What you get with a managed cluster anyway

"Managed Kubernetes" (EKS, GKE, AKS) manages the control plane. It does not manage the rest, and the rest is where the time goes. For a production cluster you will own:

  • Node groups, their instance types, their AMI updates, their autoscaling.
  • Networking: a CNI, an ingress controller, a load balancer per ingress or a shared one, cert-manager for TLS, external-dns for records.
  • Observability: metrics-server, a logging agent, and everything you'd have needed anyway.
  • Security: pod security standards, network policies, IRSA or workload identity for AWS access, image scanning, an admission controller if you're serious.
  • Upgrades: Kubernetes releases three times a year and managed providers drop support for old versions about a year after release. Every upgrade touches the API server, the nodes and, occasionally, the manifests, because something got deprecated.
  • Deployment tooling: Helm or Kustomize, an environments strategy, and probably ArgoCD or Flux once "kubectl apply from a laptop" has bitten you once.

None of this is hard for people who've done it. All of it is a permanent tax on a team that hasn't, and on a team of three it's a person. We've watched it happen: the strongest engineer becomes the cluster engineer, the product velocity halves, and eighteen months later they have a beautiful platform under a product that stopped growing.

Managed Kubernetes · what you still own node groups · AMIs · autoscaler CNI · ingress controller · load balancers cert-manager · external-dns metrics-server · logging agent pod security · network policies · workload identity 3 upgrades a year · deprecations in manifests Helm or Kustomize · ArgoCD or Flux ≈ one engineer, permanently Containers on a managed runtime · what you own a Dockerfile per service a health route that says "ready" when it's ready a service definition in CDK: image, size, min/max autoscaling on requests or CPU the same observability agent as a sidecar or a layer TLS, load balancing, node OS: not yours upgrades: change the base image tag ≈ a day a month

What we build instead

A good Dockerfile, and a managed runtime that takes an image and gives back a URL. On AWS that's App Runner for the simple case and ECS Fargate when you need a VPC-internal service or more control; the equivalents on the other clouds are Cloud Run and Container Apps, and the argument is identical.

The Dockerfile is where the engineering effort goes, and it's effort that would have been spent on Kubernetes too: multi-stage build, a non-root user, a pinned base image, a HEALTHCHECK, no secrets baked in, an image under 200 MB so cold starts are fast. We've written about the Node version of this. Get it right once and it's the same file on any runtime, including Kubernetes if you ever do move.

The runtime does what the cluster would have: pulls the image, runs N copies, health-checks them, replaces the dead ones, scales between min and max, terminates TLS, routes traffic. It does it with a service definition of about twenty lines of CDK, and it does it without a node OS for you to patch.

The rest of the stack doesn't change. Aurora or DynamoDB for data, SQS for queues, Lambda for the truly event-shaped things, the OpenTelemetry collector for observability. Nothing in that list ever needed a cluster.

The cost, honestly

The usual defence of Kubernetes on cost is that Fargate and App Runner charge a premium per vCPU-hour over raw EC2. True. Here's what that premium buys at the scale in question, for three services with two warm instances each in production:

Managed Kubernetes (EKS) Containers on App Runner / Fargate
Control plane $73 / month $0
Nodes / compute 3 × m6i.large ≈ $210, mostly idle for headroom 6 × 1 vCPU / 2 GB ≈ $250 at App Runner rates
Load balancer 1 ALB ≈ $25 + LCUs included (App Runner) or the same ALB (Fargate)
Add-ons that consume node capacity ingress, cert-manager, DNS, metrics, logging ≈ 1 node's worth none
Upgrades and patching 3 cluster upgrades / year, node AMI rotation monthly base image tag bump
Engineering time ≈ 0.5–1 FTE ≈ 0.05 FTE
Monthly, all in ~$310 + $5,000–10,000 of engineer ~$275 + ~$500 of engineer

The infrastructure lines are within $50 of each other. The engineering line is the whole comparison. At three services, Kubernetes is not more expensive to run; it's more expensive to own, by an order of magnitude, and the premium per vCPU-hour is noise against that.

When the answer changes

We're not against Kubernetes. We run it, for clients who need it. The answer flips when one of these is true, and it usually takes two:

  • Many services, many teams. Above roughly fifteen to twenty services with several teams deploying independently, the managed runtimes' per-service configuration becomes its own sprawl, and a cluster with namespaces, quotas and a GitOps controller is genuinely simpler.
  • Workloads that don't fit the runtime's shape. Requests longer than the runtime's timeout, GPUs, stateful sets, daemons that need host access, custom networking. App Runner's 120-second limit is real; if your workload needs 20 minutes of websocket, you've outgrown it.
  • Steady, large compute. At hundreds of vCPUs running around the clock, the per-vCPU premium is no longer noise and reserved nodes in a cluster are materially cheaper. This is the cost crossover, and it's much higher than people assume.
  • Portability as a requirement. If a contract says "must be deployable to the customer's own cluster", the runtime is Kubernetes because the customer's is. This is the situation with our agent platform, which is why it ships as Helm charts and why we know the tax first-hand.
  • You already have the person. If someone on the team has run production clusters and wants to keep doing it, the ownership cost drops and the calculation changes. Be sure they want to, and be sure what happens when they leave.
Is any of these true? · 15+ services, several teams deploying independently · GPUs, long-lived connections, daemons, custom networking · hundreds of vCPUs, steady, around the clock · contract says "deploy into the customer's cluster" yes, at least one none Kubernetes, with a named ownerbudget the person before the cluster a good Dockerfile + a managed runtimeApp Runner · Fargate · Cloud Run · Container Apps · revisit in a year The Dockerfile is the same in both branches. Nothing you build on the bottom branch is wasted if you later take the top one.

The objection: "but we'll have to migrate later"

Maybe. Here's what a later migration from a managed runtime to Kubernetes actually involves, for a team that built it the way described: the Dockerfiles are unchanged; the health routes are unchanged; the environment variables and secrets references become a ConfigMap and an ExternalSecret; the twenty lines of CDK per service become forty lines of Helm values. The observability agent moves from a sidecar to a DaemonSet with the same config. It's a two-week project for someone who's done it, and by the time you need it you'll have the person, because needing it is what justifies hiring them.

What isn't recoverable is the eighteen months of a small team's attention spent on a cluster it didn't need. That's the cost we're actually arguing about.

The short version

Kubernetes is a scheduler for fleets. If you don't have a fleet, run your containers on something that gives you a URL, put your effort into the Dockerfile and the health check, and revisit the question when you have fifteen services, a GPU, or a customer who insists. Until then, the most senior engineer on your team should be building the product.

If you're deciding this right now and want a second opinion that isn't selling either answer, talk to us.