Every backend team I know is stuck somewhere on the same ladder. At the bottom sits a docker-compose.yml holding together an API, a worker, a database, and a cache. At the top sits Kubernetes, radiating an aura of "real engineering." And somewhere in between is a consultant, a conference talk, or a platform team telling you that you will eventually need to climb.
I run a small pile of self-hosted services for my AI agent infrastructure, and I keep a running list of the decision points where Compose stops being enough. Because in 2026 the ladder is broken in a specific way: Docker Compose got genuinely good at the bottom, self-hosted Kubernetes got dramatically cheaper at the top, and the middle stayed exactly as confusing as it was in 2019.
Full disclosure before the numbers: this is a researched comparison built from published documentation, independent benchmarks, and teams that have publicly documented their migrations, not a from-scratch benchmark run of my own. Every claim below links to its source. What you get is the decision framework I distilled from all of it, and a concrete checklist for the day Compose actually runs out of road.
The upgrade nobody noticed: Compose Watch is stable and it changed the deal
The single biggest change to this comparison in the last two years has nothing to do with Kubernetes. It is docker compose watch, stable since Compose v2.22 and shipped inside the Docker CLI since v2.32, which syncs changed files into running containers, rebuilds on dependency changes, and restarts on config changes. No more volume-bind-mount gymnastics, no more docker compose up --build after every edit.
The setup lives in the compose file itself:
services:
api:
build: ./api
ports:
- "8000:8000"
develop:
watch:
- action: sync
path: ./api/src
target: /app/src
- action: rebuild
path: ./api/package.json
worker:
build: ./worker
depends_on:
redis:
condition: service_healthy
develop:
watch:
- action: sync+restart
path: ./worker/config
Three actions cover the whole loop:
- sync copies changed files into the running container. Fast, no restart.
- sync+restart copies, then bounces the process. Good for config files.
- rebuild rebuilds the image when a dependency manifest changes. Slow on purpose, so it only fires on the file that justifies it.
For local development this closes most of the gap that used to push people toward Kubernetes tooling like Skaffold or Tilt. Your local loop is Compose's home turf, and in 2026 it is a genuinely polished one.
What Compose still cannot do, and why that list is shorter than you think
Strip away the marketing and Compose's hard limits come down to five things:
- Single host. No supported way to spread containers across machines without Docker Swarm, and Swarm is in maintenance mode.
-
No rolling deploys.
docker compose up --force-recreateon a single host means downtime, or you hand-roll a health-check loop. -
No self-healing across failure.
restart: alwaysrestarts a crashed container on the same box. If the box dies, everything on it is down until a human notices. -
No metrics-driven autoscaling.
docker compose up --scale api=5is manual and duplicates replicas on the same host, which mostly just divides the same RAM five ways. - No RBAC or audited secrets. Compose has no built-in access control, and secrets are environment variables or bind-mounted files.
Now the honest counterweight: most single-team backend products never hit four of those five limits in a way that hurts. One well-provisioned VPS or a modest dedicated box runs 10 to 50 containers comfortably, with roughly 50 MB of overhead for the Compose daemon versus 2 to 4 GB for a Kubernetes control plane. If your database, your queue, and your API all fit on one machine with room to spare, multi-node scheduling solves a problem you do not have.
The cost of being wrong in each direction is not symmetric, either. Running Kubernetes when you do not need it is a standing tax: a control plane to babysit, YAML to maintain, and an on-call learning curve. Running Compose past its breaking point is a sudden outage, which is worse but rarer and usually cheap to fix by finally doing the migration you postponed.
What self-hosted Kubernetes actually costs in 2026
Here is the part that changed most: the "Kubernetes is expensive" half of the old advice has aged badly, because at the entry tier the managed control plane is effectively free across the board now.
- Google GKE charges $0.10 per cluster per hour for the managed control plane, about $73 for a full month, but the GKE free tier fully credits one zonal Standard cluster per billing account, so a single small cluster runs its control plane at $0.
- DigitalOcean DOKS includes the control plane for free on the standard tier, with an optional high-availability control plane at $40 per month. Linode (Akamai) and Vultr are also $0, and a realistic small production DOKS cluster lands around $63 per month: two 4 GB nodes, a $12 load balancer, and 30 GB of block storage. The catch is that the control plane was never the expensive part. Nodes, load balancers, and block storage are the bill, and an HA control plane costs more than half of everything else combined.
- OVHcloud and Scaleway also offer free control planes on their managed Kubernetes, charging only for the worker nodes.
- k3s remains the self-hosted escape hatch: a single ~70 MB binary that turns a few cheap VPS nodes into a conformant cluster, with high availability supported through the built-in distributed SQL datastore or an external database.
So the real 2026 question is not "can we afford Kubernetes." It is "do we want to operate it." Even a free control plane still leaves you node upgrades, workload migration, ingress, cert rotation, and a monitoring stack. That is the part people underestimate, and it is labor, not money.
Where each tool wins: the honest scorecard
-
Local development loop. Compose, and it is not close. Watch-mode sync, dependency-aware rebuilds,
depends_on: condition: service_healthy, and a single YAML file every developer already knows. - CI integration tests. Compose. Spinning the full stack in a pipeline takes one command and tears down cleanly.
- Single-server production for one product. Compose, usually. Restart policies, healthchecks, and a reverse proxy cover the uptime needs of most products below the "we lost a sales deal because of downtime" threshold.
- Zero-downtime deploys. Kubernetes. Readiness-gated rolling updates that wait for new pods to be healthy before draining old ones are a core primitive, not something you script yourself on a Friday afternoon.
- Multi-service scaling on real traffic. Kubernetes. The Horizontal Pod Autoscaler reacts to load; Compose asks a human to type a scale command.
- Strict multi-tenancy or compliance. Kubernetes. NetworkPolicies, RBAC, and auditable secrets management are table stakes there.
- Bare-metal or edge clusters. Kubernetes via k3s. Compose has no story here at all.
The trigger list: when to actually migrate
After going through a dozen public migration write-ups, the triggers cluster into a short list. Two or more of these true is the point where Compose stops being a deployment strategy and starts being a liability:
- You need zero-downtime deploys, and recreating containers during business hours has actually cost you something.
- You have lost production time to a host failure that required a human to intervene.
- Traffic patterns mean you need to add or remove capacity more than once a week, by hand.
- More than one team ships to the same hosts and you cannot answer "who changed what" with an audit log.
- Your compliance requirements demand RBAC and secrets auditing.
- You are scheduling work across more than one machine, full stop.
If none of those are true, the boring answer is the right one: stay on Compose, spend the saved operational hours on the product, and revisit when the list grows.
The migration path, if you do pull the trigger
The teams that documented their moves converged on the same sequence, and it is worth having in your back pocket even if you never use it:
-
Inventory the compose file. Every service, volume, network, and
depends_oncondition maps to a Kubernetes concept. Compose conditions become init containers or readiness probes. -
Convert mechanically first.
docker compose configto normalize, thenkompose convertfor a first-pass set of manifests, or a Helm chart if you would rather template from day one. - Replace env-var config with ConfigMaps and Secrets. This is where most of the manual work hides, because Compose lets you be lazy about it.
- Add probes before you need them. Liveness and readiness endpoints are the whole point of the migration. A service without a readiness probe will get traffic before it can serve it.
- Set resource requests and limits. Compose never forced the conversation; Kubernetes does. Do it deliberately rather than accepting defaults.
- Cut over service by service, not big bang. Keep Compose running the parts that work, move the stateless API first, keep stateful stores managed or untouched.
- Keep the compose file. It remains the local dev and CI contract even after production moves. That is the hybrid endgame most teams land on.
What I would do with a four-service backend in 2026
- Develop on Compose with watch. The fastest inner loop with the least configuration.
- Test in CI with Compose. One command, full stack, throwaway.
- Deploy on a single well-monitored host with Compose until at least two triggers from the list above fire.
- Then go managed Kubernetes with a cheap or free control plane, starting with the stateless services, and keep Compose for dev and CI.
The ladder is not a ladder anymore. It is a fork in the road, and the fork happens much later than the conference talks want you to believe.
I write about backend engineering, deployment, and AI infrastructure every week. Subscribe, it is free, and it keeps these deep dives coming.
What is running your production right now: a compose file, a Kubernetes cluster, or something in between? And has anyone actually regretted staying on Compose longer than the internet said they should?
If you found this useful, save it for the next time someone on your team proposes "we should move to Kubernetes."
Top comments (1)
I've been running the same experiment in reverse. My stack sits in production behind Compose, and
watchclosed exactly the loop that made me look at k3s twice. Thesync+restartaction for config files is the piece I undervalued: without it you end up rebuilding the whole worker image for a five-line YAML change.The asymmetry argument is what I'd underline. The "box dies and everything on it is down" failure mode is real, but it's one well-understood risk you can buy down with a second host and a restore drill, rather than a permanent operational surface. Have you found a middle ground that keeps the
depends_on: condition: service_healthyguarantees while a second host can take over, or is that precisely the line where Compose has actually run out of road?