What Docker Compose and Kubernetes actually are
Docker Compose defines a multi-container application (its services, networks and volumes) in one YAML file and runs it against a single Docker engine. Its commands cover the application lifecycle on that host: start, stop and rebuild services, view their status, stream their logs, and run one-off commands inside a service. When you run docker compose up, Compose compares the file with the containers on that engine and creates or recreates what differs. After that, it stops working. Once the command exits, nothing watches the application except the Docker engine's restart policies.
Kubernetes manages containerized workloads across a cluster of nodes. You submit desired state, such as "three replicas of this image, reachable under this name." Controllers then compare that state with what is actually running and keep correcting the difference. If a container crashes, fails its health check or loses its node, the system acts without anyone running a command.
The example below shows the difference in scope. The Compose file describes one service in a handful of lines. The Kubernetes version needs two objects. A Deployment declares the pods and their replica count. A Service gives those pods a stable network identity. Kubernetes separates these two concerns because pods are disposable and can be rescheduled onto any node, so clients need an address that outlives any individual pod. Compose does not need this layer because everything shares one host and one network.
Both tools are container orchestration. The difference is scope: Compose works on one machine, and Kubernetes works on a cluster that heals itself. Choose between them based on which of those scopes your failure model requires.
# compose.yaml: one service on one Docker engine
services:
web:
image: nginx:1.27
ports:
- "8080:80"
restart: unless-stopped # after 'up' exits, only the engine's restart policy watches this
---
# deployment.yaml: desired state that controllers keep reconciling across the cluster
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3 # controllers recreate pods until three are running
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
---
# service.yaml: a stable name and address that outlives any individual pod
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # sends traffic to whichever pods currently carry this label
ports:
- port: 80
targetPort: 80
The same nginx web service written as a single-host Compose file and as a Kubernetes Deployment plus Service. Kubernetes splits the replicated, reschedulable pods from the stable network identity that clients use to reach them.
How they work: one host versus a cluster
With Compose, the unit you deploy to is one Docker host. You run docker compose up once, and the Compose CLI tells that host's Docker daemon to create the containers, networks and volumes. After that, Compose does not keep running to watch over the stack. Kubernetes works differently. You send desired state to an API server, and control-plane components keep that state stored. Controllers then compare what is actually running against it and correct any difference, while the scheduler assigns pods to whichever worker nodes have room.
Docker Compose Kubernetes
+----------------------+ +-------------------------------+
| Host A | | Control plane |
| Docker daemon | | API server, scheduler, |
| [web] [api] [db] | | controllers, etcd |
| volume: ./data | +---------------+---------------+
+----------------------+ schedules | reconciles
+-----------+-----------+
v v v
[Node 1] [Node 2] [Node 3]
pod api pod api pod web
pod web pod db
Failover, scaling and storage all follow from that difference.
Failover. In Compose, a restart policy tells the Docker daemon to restart a container that crashes. If Host A itself goes down, the daemon goes down with it, and nothing else exists to recover the stack. In Kubernetes, a controller sees that pods are missing from a lost node and creates replacements on the nodes that are still healthy.
Scaling. Compose can run several replicas of a service, but all of them share one machine's CPU and memory. Kubernetes places replicas across nodes based on their resource requests, so you add capacity by adding nodes.
Storage. A Compose volume is a directory on Host A, and it can't move to another machine without manual work. Kubernetes separates the storage claim from the node, so a network-backed volume can be attached to a replacement pod on another node. Node-local volumes remain tied to their node.
DOCKER COMPOSE: one host KUBERNETES: cluster
docker compose up (one-shot) desired state -> API server
| |
v v
+------------------------+ +------------------------------+
| Host A | | Control plane |
| Docker daemon | | API server -> etcd |
| restart policy only | | scheduler: place pods |
| [web] [api] [db] | | controllers: reconcile |
| volume: ./data (local) | | actual vs desired, fix drift |
+------------------------+ +------------------------------+
Host A fails: | schedules, reconciles
daemon and stack go down; +----------+----------+
nothing elsewhere recovers. v v v
+--------+ +--------+ +--------+
Scale: all replicas share | Node 1 | | Node 2 | | Node 3 |
Host A's CPU and memory. | api | | api | | web |
| web | | db | | |
Data: ./data lives on Host A. +--------+ +--------+ +--------+
If Node 2 fails, controllers
recreate api and db on Node 1/3;
db needs storage reachable there.
Compose hands a one-time request to a single host's Docker daemon, so failover, scaling and data all stop at that host. Kubernetes keeps desired state in a control plane that schedules pods across nodes and keeps reconciling, so lost pods are recreated elsewhere if their storage can follow.
Feature-by-feature: what Compose can and cannot approximate
The table maps each capability from the Kubernetes overview [2] to the closest thing Compose offers on a single host [1].
| Kubernetes capability | Nearest Compose equivalent | What's missing |
|---|---|---|
| Service discovery and load balancing | Service-name DNS on the project network; scaled replicas share a name | No health-aware traffic routing |
| Storage orchestration | Named volumes and bind mounts | Volumes are tied to one host |
| Automated rollouts and rollbacks | Pull and recreate containers | No controlled rate, no automatic rollback |
| Bin packing by requests | Per-container CPU and memory limits | No placement, because there is only one node |
| Self-healing | Restart policies and healthcheck |
Nothing reschedules work when the host dies |
| Secrets and config | File-mounted secrets and configs |
No API, no access control |
| Batch jobs | One-off docker compose run |
No retries or completion tracking |
| Horizontal scaling | --scale on one machine |
Capped by that machine's capacity |
Self-healing is where the gap matters most. A restart policy restarts a container after its process exits. A healthcheck marks a container unhealthy and can gate depends_on, but Docker does not restart an unhealthy container on its own. You need the process to crash, or you need an external watchdog. More importantly, every guarantee in the table assumes the host survives. If the machine or its disk fails, no Compose setting recovers the stack.
Kubernetes has a larger ecosystem, and that has a cost. Ingress controllers, CNI plugins and operators let you choose how routing, networking and stateful services behave. Each one is also a separately versioned component. You have to upgrade it, check its compatibility with your cluster version, and debug it at 3 a.m. Count every add-on you plan to install as a dependency you will be running for the long term.
Who should use which: decision questions for your team
No headcount or request rate flips the answer. Ask these questions in order, because each one is about a failure or a constraint, not a feature you might like to have.
| Question | Points to Compose if… | Points to Kubernetes if… |
|---|---|---|
| Can you tolerate downtime when one machine fails? | A restore from backup or a rebuilt host within your recovery target is acceptable | Losing a node must not take the service down |
| Do you need zero-downtime rolling deploys or canaries? | A brief restart during deploys is fine, or a load balancer in front covers it | Gradual, health-gated rollouts are a hard requirement |
| Does load exceed one host? | One machine, sized up, handles peak with headroom | You need to spread replicas across several nodes |
| Who operates it? | Nobody on the team wants to own a cluster | You have operators, or budget for a managed control plane |
| How many services and teams deploy independently? | One team ships a handful of services together | Several teams need isolated namespaces, quotas and release cadences |
Choose Compose when every answer lands in the left column. Avoid it when a single host is a failure you cannot absorb. Choose Kubernetes when two or more answers land on the right and you can staff it. Avoid it when you are adopting it for features nobody has asked for yet.
Either choice can go wrong. Adopt Kubernetes early and you lock in manifests, controllers and on-call knowledge that a small system may never need. That cost recurs with every upgrade. Stay on Compose too long and the migration happens under pressure, often after an outage. Stateful services, host-mounted volumes and assumptions about localhost all have to be untangled at the same time.
Running Compose in production without fooling yourself
If you stay on Compose, set it up so it fails in ways you can predict. Pin every image to a specific tag or digest. With latest, a routine docker compose pull can bring in a version nobody tested. Set restart: unless-stopped so services come back after a crash or a host reboot. Add a healthcheck to each service and use depends_on with condition: service_healthy, so the app waits for its dependencies to be ready. Plain Docker does not restart a container just because it is marked unhealthy. You still need monitoring that alerts on that state.
Run the database somewhere other than this host if you can, such as a managed service or a separate machine. Then losing the host means rebuilding stateless containers, not recovering data. If a stateful container has to stay, schedule backups and test restoring them. Put a reverse proxy in front to handle TLS and routing. Deploy with a script, not commands typed by hand: docker compose pull, then docker compose up -d, then check docker compose ps and the health endpoints.
The limits are real. The host is a single point of failure, and someone has to recover it by hand. up -d replaces containers in place. Zero-downtime deploys need extra tooling or careful work in the proxy.
Teams on Kubernetes should still use Compose for local development and CI. Watch for drift between dev and prod in three places:
- Config: different env files.
- Networking: service names versus cluster DNS.
- Secrets: plaintext
.envfiles versus mounted secrets.
That drift is where bugs that only show up in production come from.
# compose.yaml: single-host production setup for a small team
services:
api:
# Pin an exact version, never :latest, so `docker compose pull` stays predictable
image: registry.example.com/shop-api:1.4.2
restart: unless-stopped # come back after a crash or host reboot
env_file: .env.production # placeholder values only; keep this file out of git
environment:
# The database is managed and lives off this host, so losing the host loses no data
DATABASE_URL: postgres://app:CHANGE_ME@db.internal.example.com:5432/shop
healthcheck:
# Assumes the image ships curl and serves a /healthz endpoint
test: ["CMD", "curl", "-fsS", ""]
interval: 10s
timeout: 3s
retries: 5
start_period: 20s
expose:
- "8080" # reachable only by the proxy, not published on the host
proxy:
image: caddy:2.8 # pinned major.minor; pin a digest for stricter control
restart: unless-stopped
ports:
- "80:80"
- "443:443" # the proxy terminates TLS for every service
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # keeps issued certificates across restarts
depends_on:
api:
condition: service_healthy # don't route traffic until the API reports healthy
volumes:
caddy_data:
# Note: Docker marks a failing container "unhealthy" but does not restart it.
# Alert on that state from your monitoring.
A hardened compose.yaml with pinned images, restart policies, a healthcheck gating a TLS-terminating reverse proxy, and an external managed database instead of a stateful container on the host.
Impact on cost and engineering time
Writing manifests is the cheap part of Kubernetes. The recurring cost is the work around them:
- Upgrades: minor-version upgrades can remove APIs your manifests still use.
- Networking and ingress: the CNI plugin and ingress controller must be understood by someone when traffic silently drops.
- RBAC: roles drift from who actually needs access.
- Observability: a metrics and log pipeline has to cover many nodes, not one.
- Storage: storage classes and reclaim policies decide whether deleting a claim also deletes your data.
- On-call knowledge: every engineer who gets paged needs to read pod events and probe failures under pressure.
A managed offering runs the control plane for you. It does not tune your probes, size resource requests or debug network policies, and it adds a cluster fee on top of node costs.
Illustrative estimate (assumed numbers, not a benchmark)
Monthly cost = infrastructure + (platform hours × hourly cost)
| Input (assumed) | Single-host Compose | Small managed cluster |
|---|---|---|
| Infrastructure | $150 (one VM) | $550 (fee + 3 nodes + load balancer) |
| Platform hours/month | 6 | 30 |
| Hourly engineering cost | $100 | $100 |
| Total | $750 | $3,550 |
Substitute your own figures. The hours term usually dominates the total.
Compose saves time day to day. It costs time during failures, because replacing a host and restoring data are manual steps. Once Kubernetes is tuned, it saves time on failover and rollouts. It costs time on upgrades and on debugging the layers between the client and the container.
A misconfigured cluster can be less reliable than a well-run single host. Missing resource requests can cause pods to be evicted, and an overly strict readiness probe can pull healthy pods out of service.
Signals it's time to migrate, and what the move involves
Signals worth acting on
- The same host keeps taking you down. If postmortems keep tracing back to one machine (a full disk, a kernel panic, an instance retired by the provider), a better restart policy won't help. You need a scheduler that moves work to another node.
- Capacity is a human job. Someone decides which service runs on which box, edits files, and hopes the memory fits. Kubernetes' bin packing exists to remove that decision.
- Deploys need a maintenance window. Recreating containers on a single host drops connections. Rolling updates gated on readiness avoid that.
- Teams queue behind one compose file. Merge conflicts and shared deploys mean the file has become a coordination bottleneck. Separate namespaces and per-team manifests split ownership.
What the move involves
A Compose service maps roughly to a Deployment plus a Service, but not one-to-one, so expect to rewrite rather than translate:
- Networking: Compose gives every service DNS by name on a default network. In Kubernetes you define Services explicitly, and external traffic needs an ingress layer.
- Volumes: Named volumes become PersistentVolumeClaims backed by your provider's storage.
- Secrets and config: Env files and mounted secrets become Secrets and ConfigMaps.
Stateful workloads stay hard either way. A database on a PersistentVolume still depends on the provider's storage behavior. It still needs backups, restore drills and a failover plan.
Alternatives
If you need multiple hosts but not Kubernetes' API surface, consider two options. A managed container platform runs your images for you. A simpler multi-host orchestrator has fewer concepts to learn. Both give up flexibility in exchange for less to operate.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to docker compose vs kubernetes, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Changing a shared variable is one edit and one review instead of editing every service's
.envby hand.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.