Everyone says Compose is for dev only. We ran it in production for two years on a single node and it was the right call, until the day it very much wasn't.
A consultant told our founder that running Docker Compose in production was "amateur hour" and we needed Kubernetes. At the time we were serving 40,000 daily active users off a single Compose file on one beefy VM, with 99.95% uptime over the prior year. We ignored the consultant for another 18 months, and it was the correct decision. Then our traffic pattern changed and Compose stopped fitting, cleanly and obviously. Both facts are true, and the interesting part is knowing which situation you're in.
The dirty secret is that a huge number of production workloads fit on one machine. A modern VM with 16 vCPUs and 64GB of RAM is a lot of computer. If your app, its background workers, and a couple of supporting services fit there with headroom, an orchestrator's job (scheduling containers across many nodes) is solving a problem you don't have. You're paying the full operational cost of Kubernetes to schedule pods onto the one node you own.
Our production Compose file wasn't a dev toy. It was tuned for it:
services:
api:
image: registry.devopsness.net/api:2026.06.30
restart: unless-stopped
deploy:
resources:
limits:
cpus: "4"
memory: 4g
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
depends_on:
db:
condition: service_healthy
The pieces that make this production-grade: restart: unless-stopped so containers come back after a crash or reboot, real healthchecks so Docker knows when a service is actually ready, depends_on with condition: service_healthy so the API doesn't start hammering a database that isn't up yet, and log rotation so a chatty service doesn't fill the disk and take the box down. That last one has killed more single-node deployments than anything else.
The usual objection is that Compose can't do zero-downtime deploys. It can, with a caveat. docker compose up -d recreates changed services, and with a healthcheck plus a small script you get rolling behavior on a single service by scaling up before scaling down:
docker compose pull api
docker compose up -d --no-deps --scale api=2 --no-recreate api
sleep 15 # let the new container pass its healthcheck
docker compose up -d --no-deps --scale api=1 api
We fronted it with an Nginx that health-checked upstreams, so traffic only went to a container passing /healthz. Not elegant, but it delivered zero-downtime deploys for two years. The honest limitation: this is a manual dance, and it only works because we had exactly one node and one service that mattered.
The move to Kubernetes wasn't ideology. It was three specific things arriving at once.
First, a marketing campaign meant traffic could 5x in an hour, and we needed to add capacity automatically. Compose has no autoscaling and no second node to scale onto. Second, a single VM is a single point of failure, and we'd promised an enterprise customer a multi-AZ SLA that one machine physically cannot meet. Third, we grew to four teams deploying independently, and a shared Compose file became a merge-conflict and blast-radius problem, because one team's bad deploy could take down the whole node.
None of those are "Compose is bad." They're "our requirements outgrew one machine." When you need scheduling across nodes, automatic failover across zones, and independent team deploys, you need an orchestrator, and that's genuinely what Kubernetes is for.
Because we'd kept the Compose file clean, moving to Kubernetes was mostly mechanical translation: services became Deployments, the healthchecks became readiness and liveness probes almost verbatim, resource limits carried over directly, and depends_on logic moved into init containers. Kompose got us a rough draft we then cleaned up by hand. Having disciplined resource limits and healthchecks already in place made the port far less painful than starting from a sprawling Compose file with no limits at all.
Run Compose in production if your whole workload fits comfortably on one node, you can tolerate the rare reboot's worth of downtime, and one team owns the deploys. That describes a lot of real businesses, and reaching for Kubernetes there is cargo-culting operational complexity you'll pay for daily. Move to an orchestrator when you cross a real line: you need more than one node's capacity, you need automatic multi-zone failover for an SLA, or you have multiple teams that need isolated blast radius. Don't switch because a consultant made you feel amateur. Switch because you hit a requirement one machine cannot satisfy, and keep your Compose file clean so that when the day comes, the move is a translation and not a rebuild.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Static service tokens leaked into logs and never rotated. SPIFFE identities plus SPIRE-issued SVIDs gave us short-lived certs and killed the shared-secret sprawl.
We moved 40 services off the nginx Ingress controller onto Gateway API without a single dropped connection. Here's the routing overlap trick that made it boring.
Explore more articles in this category
Every CI/CD platform claims to be fast and easy. The real differences are in pricing, self-hosting, and where each one falls apart at scale. This is the map.
Woodpecker forked Drone when the license changed. Here's how the two compare for small teams and homelabs that just want simple container-native CI.
Buildkite runs the control plane and lets you own the compute; Actions keeps everything close to your repo. Here's how they actually differ once you scale.
Evergreen posts worth revisiting.