The production stack has been Docker Compose on a VPS since 2019. Nothing about it is broken, which is precisely why the question of moving to an orchestrator comes up roughly once a year — the pressure is from the industry default rather than from anything the setup is failing to do.
The symptom
the proposal, in its fourth year:
"we should move to Kubernetes"
the reasons given, each time:
2022 scaling
2023 self-healing
2024 it is what everybody uses
2025 hiring — candidates expect it
none of these is wrong. three of them are about
something other than this system.The hiring argument is the strongest one and it is not a technical argument, which is worth being explicit about rather than dismissing. The other three are about capabilities, and the useful question is which of those capabilities this system needs.
Why it happens
A default exerts pressure independently of whether it fits. Nobody has to justify choosing the industry standard and everybody has to justify not choosing it, which means the decision is re-litigated annually by whoever has most recently read about it.
The fix
What Compose does not give us
rolling updates not built in
scheduling no concept of a second host
self-healing restart: unless-stopped is
process-level, not health-level
service discovery DNS within one network only
secrets environment variables and a file
autoscaling none
four of these matter to somebody. the question is
whether they matter here.Which of them we need
rolling updates yes — 20 lines of shell, below.
a second host no. one application server, one
database, one cache. a second host is
an availability decision nobody has
made.
self-healing partially. a container that crashes
restarts; one that is unhealthy does
not. covered by an alert rather than
by automation.
autoscaling no. load varies threefold across a
day and the peak fits.The unhealthy-but-running container is the genuine gap, and covering it with an alert rather than an automatic restart is a deliberate choice — an automatic restart of an unhealthy container hides a bug that a human would investigate. That is defensible at this size and would not be at fifty services.
The rolling update, in twenty lines
#!/usr/bin/env bash
set -euo pipefail
service=app
replicas=2
for i in $(seq 1 "$replicas"); do
docker compose up -d --no-deps --no-recreate
--scale "$service=$((replicas + 1))" "$service"
new=$(docker compose ps -q "$service" | tail -1)
./bin/wait-healthy "$new" 40 || {
docker compose up -d --scale "$service=$replicas" "$service"
exit 1
}
old=$(docker compose ps -q "$service" | head -1)
docker stop "$old" && docker rm "$old"
done
docker compose up -d --scale "$service=$replicas" "$service"
Scale up, wait for health, remove the oldest, repeat — and the part that is easy to get wrong is the health wait, because a container that is running is not a container that is serving. Twenty lines is not an argument that orchestrators are unnecessary; it is an argument about what two replicas on one host actually require.
What the alternative would cost, properly
a managed cluster, costed over a year:
control plane £70 + three nodes £220 + a load
balancer £18 = £308/month, against £48 for the VPS.
the learning 3 people × ~2 weeks
the migration estimated 4-6 weeks
the manifests, ingress, secret management and
monitoring stack — ongoing
and the operational surface: a cluster fails in ways a
VPS does not, and nobody here has debugged one at 03:00.Six times the infrastructure cost is the least interesting number. The learning and the operational surface are the real ones — three people who have never debugged a cluster under pressure is a capability gap that a migration creates rather than resolves, and it lands during the first incident rather than during the migration.
What would change the answer
written into the decision record, so the fifth
conversation is a check rather than a debate:
a second host that must fail over automatically
— an availability requirement we do not have
more than about six services
— currently four
a team large enough that one person can own the
platform
— currently three, all of whom write application
code
a customer or contractual requirement naming it
a load pattern that genuinely needs autoscaling
none of these is true. any one of them changes the
answer, and the first is the most likely.The failure drill
# quarterly, on a Saturday
$ docker compose kill app && ./bin/measure-recovery
restarted by the runtime: 2 first request: 14s
requests failed: ~40
$ ssh app-1 'sudo reboot' && ./bin/measure-recovery
first request: 94s
# and the one the drill exists for
$ ssh app-1 'sudo systemctl stop docker'
→ total outage until manual intervention. 4 minutes,
including somebody being alerted and responding.Ninety-four seconds to recover from a reboot and four minutes from a runtime failure requiring a human is the honest availability of this arrangement. That is the number the decision rests on, and it is a number nobody had measured before the drill — the argument had been conducted entirely in the abstract for three years.
Verifying it worked
$ ./bin/deploy && ./bin/measure-downtime
requests failed during the rolling update: 0
duration: 38s
$ ./bin/uptime-report --since=2024-09
availability: 99.94%
incidents: 3
of which caused
by the runtime: 0
$ cat docs/adr/018-container-orchestration.md | head -4
# ADR-018: Docker Compose on a single host
## Status
Accepted 2022-03. Reviewed 2023-09, 2024-09, 2025-09.Three reviews recorded on one document is what turns an annual argument into an annual check, and the availability figure is what the argument should have been about from the start. Zero incidents caused by the runtime in a year is not proof that an orchestrator would not be better — it is evidence that the current arrangement is not the constraint.
What this costs
A position that has to be defended annually, and a ceiling that is genuinely there. Everything above four services, one host or three people is a different conversation, and the setup has no growth path — the day the answer changes, the migration is the full one that has been deferred for four years.
The hiring argument is also the one this does not answer. Candidates expect orchestration experience and this system does not provide it, which is a real cost to the people working on it and is not something the decision record can dismiss. The honest position is that it is a trade, and that the people making it are the ones paying.