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
We used to ship code and turn it on in the same breath, so every deploy was a bet. Feature flags split those two events apart and made rollbacks a config toggle.
Our best engineer quit citing on-call. We rebuilt the whole thing: saner rotations, runbooks that actually help at 3am, and escalation that doesn't punish asking for help.
Our early postmortems quietly assigned blame and taught people to hide mistakes. Here's the template and the facilitation rules that finally made them honest and useful.
Evergreen posts worth revisiting.