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 .env files 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 .env by 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.