Comparisons

Docker Compose vs Kubernetes: one host or a cluster?

Docker Compose vs Kubernetes: Compose runs containers on one host from one file, Kubernetes schedules them across a cluster. Costs, k3s, Swarm and kompose.

Published ~9 min read
On this page

Short answer: Docker Compose starts a set of containers on one host from a single YAML file. Kubernetes runs containers across a cluster of machines and keeps them in the state you declared, restarting, rescheduling, scaling and updating them for you. For one VPS and a handful of services, Compose is the right tool; Kubernetes pays off once you need several nodes.

the same web app, both ways
# Docker Compose: one host, one file
docker compose up -d                 # reads compose.yaml in this directory
docker compose ps

# Kubernetes: a cluster, many objects
kubectl apply -f k8s/                # Deployments, Services, Ingress, volumes...
kubectl get pods -o wide             # which node each copy landed on

They are not rivals at the same level. Both run the same OCI images (see Docker image vs container), and a Compose file can be the first draft of a Kubernetes deployment. The question is whether you need a scheduler that spans machines, and whether you can afford to run one.

Docker Compose vs Kubernetes at a glance

Docker ComposeKubernetes
Runs onOne Docker hostA cluster of nodes (one node is possible)
Configurationcompose.yaml, one file per projectMany objects: Deployment, Service, Ingress, PersistentVolumeClaim, often through Helm or Kustomize
A container crashesThe restart policy starts it again on the same hostThe controller replaces the pod, on another node if needed
The host diesEverything is down until the host is backPods are rescheduled onto healthy nodes, by default after about five minutes
Deploying a new versionContainers are recreated: a short gapRolling update, and kubectl rollout undo to go back
Scaling--scale web=3 on the same hostReplicas spread over nodes; HorizontalPodAutoscaler on CPU, memory or custom metrics
Service discoveryService names resolve on the project networkServices with a stable virtual IP and cluster DNS, balanced across replicas
StorageNamed volumes and bind mounts on local diskPersistentVolumes through a storage class; pods that move need network storage
OverheadThe Docker daemonA control plane plus agents on every node
Learning curveAn afternoonWeeks, then ongoing
Best forOne server, small teams, self-hostingSeveral nodes, zero-downtime deploys, many teams and services

What Docker Compose is good at

Compose describes an application as services, networks and volumes in one file, and the docker compose plugin turns that file into running containers on the host where you run it. Since Compose V2 the file is usually called compose.yaml, and the top-level version: key is obsolete: current Compose ignores it and prints a warning.

compose.yaml · a web app and Redis
services:
  web:
    image: ghcr.io/example/web:1.4.2
    ports:
      - "80:8080"
    environment:
      REDIS_URL: redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
    restart: unless-stopped

volumes:
  redis-data:

That small file already gives you most of what a single server needs:

  • Service discovery. Compose creates a network for the project, and web reaches Redis simply as redis.
  • Start order with health. condition: service_healthy holds web back until Redis answers its healthcheck.
  • Recovery. restart: unless-stopped brings a crashed container back, and brings everything back after a reboot, as long as the Docker service is enabled.
  • Persistent data. The named volume keeps Redis’s data when the container is replaced.
  • Updates. docker compose pull && docker compose up -d recreates only the services whose image changed.

What Compose cannot do is leave the host. Every container lives on one machine, so a hardware fault, a kernel update reboot or a full disk takes everything down at once. An update stops the old container before the new one starts, which is a gap of a few seconds for most apps. And docker compose up -d --scale web=3 starts three copies on the same host, which fails as soon as the service publishes a fixed host port like 80:8080: only one container can own port 80. The commands for daily use are in the Docker commands cheat sheet.

What Kubernetes adds

Kubernetes works from desired state: you declare “three copies of this image, reachable as web”, and controllers keep comparing that with reality and fixing the difference. The web service from the Compose file above looks like this:

k8s/web.yaml · Deployment and Service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels: { app: web }
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
        - name: web
          image: ghcr.io/example/web:1.4.2
          ports:
            - containerPort: 8080
          env:
            - name: REDIS_URL
              value: redis://redis:6379
          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 256Mi }
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector: { app: web }
  ports:
    - port: 80
      targetPort: 8080
  • Self-healing across nodes. If a pod crashes, the kubelet restarts it. If a whole node stops answering, its pods are evicted and recreated elsewhere. By default pods tolerate an unreachable node for 300 seconds before that happens, so plan for a few minutes, not instant failover.
  • Rolling updates. Change the image tag and Kubernetes starts a new pod, waits for its readiness probe, then retires an old one, until all are replaced. With maxUnavailable: 0 capacity never drops. kubectl rollout undo deployment/web goes back.
  • Service discovery and load balancing. A Service gets a stable virtual IP and a DNS name (web.default.svc.cluster.local, or just web inside the namespace), and spreads connections over every ready pod on every node.
  • Autoscaling. A HorizontalPodAutoscaler adds or removes replicas on CPU, memory or custom metrics (CPU and memory need the metrics-server add-on, custom metrics an adapter). Adding nodes automatically needs a cluster autoscaler that can order machines from your provider’s API.
  • Everything around it. Namespaces and RBAC for several teams, ConfigMaps and Secrets, Ingress or the Gateway API for HTTP routing, and an ecosystem of Helm charts and operators for databases, certificates and monitoring.

What Kubernetes costs

  • A control plane. The API server, etcd, the scheduler and the controller manager run before a single app pod does, plus the kubelet, a network plugin, kube-proxy and CoreDNS on every node. The kubeadm documentation asks for at least 2 GB of RAM per machine and 2 CPUs for control-plane machines.
  • High availability needs three. etcd needs a majority to accept writes, so a control plane that survives the loss of one machine needs three of them. A single control-plane node is a single point of failure again, just a more complicated one.
  • Storage that can move. A volume on node A does not follow a pod to node B. Databases either stay pinned to one node, which gives up the self-healing, or need replicated storage such as Longhorn or Ceph, which is a system of its own.
  • Upgrades on a schedule. Kubernetes ships three minor releases a year and patches each for roughly a year (see kubernetes.io/releases for the current dates), so a cluster needs a minor upgrade every few months to stay supported, one minor version at a time.
  • More YAML, more concepts. The two-service Compose file becomes four to six objects, before ingress, certificates and monitoring.

Small-scale options: k3s, MicroK8s, Docker Swarm

k3s

k3s is a certified Kubernetes distribution packed into one binary. A single server uses SQLite instead of etcd unless you ask for embedded etcd, and it ships with a network plugin, CoreDNS, the Traefik ingress controller and a local-path storage provisioner. Its documentation lists 2 cores and 2 GB of RAM as the minimum for a server node and 1 core and 512 MB for an agent. On a single VPS it is the quickest way to run real Kubernetes manifests:

Debian · Ubuntu · Rocky Linux · as root
curl -sfL https://get.k3s.io | sh -
k3s kubectl get nodes
k3s kubectl apply -f k8s/

Nodes on separate servers talk over the network between them. If that is the public internet, encrypt it (k3s can use WireGuard as its flannel backend) and open the API port 6443, the kubelet port 10250 and the flannel port of your backend only to the other nodes.

MicroK8s

Canonical’s MicroK8s installs as a snap (snap install microk8s --classic) and switches features on as add-ons, such as microk8s enable dns ingress. It feels most at home on Ubuntu. Canonical recommends at least 4 GB of memory for real workloads.

Docker Swarm

Swarm mode is built into Docker Engine. docker swarm init turns a host into a manager, other hosts join with a token, and docker stack deploy -c compose.yaml app runs a Compose file across them, with replicas, rolling updates from the deploy: section and overlay networks between hosts. It is far simpler than Kubernetes and reuses what you already know from Compose; the trade-off is a much smaller ecosystem and fewer people building on it, so check the current state of swarm mode on docs.docker.com before you commit a new project to it.

When Compose on one VPS is the right answer

  • One server has enough capacity. A VPS with 8 or 16 cores can run dozens of containers. Scale up before you scale out.
  • A few minutes of downtime for maintenance is acceptable. A reboot for a kernel update takes a minute or two. For most internal tools, side projects, shops and APIs that is fine when it is planned.
  • The data lives in one place. A Postgres or MariaDB container with a named volume, plus a nightly dump to another server, is simpler and safer than a database on a cluster you are still learning.
  • One to three people run it. Kubernetes earns its overhead when many teams deploy many services; for a small team it is mostly overhead.

A tested restore on a second server covers most of what people want from “high availability” at a fraction of the effort. Put a reverse proxy in front (Caddy, Traefik or nginx), and remember that ports Docker publishes skip UFW; installing Docker on Ubuntu and Debian covers the fix along with the install itself. For single containers outside Compose, see the docker run guide.

When Kubernetes is worth it

  • The workload no longer fits on one machine, or must survive losing one.
  • Deploys must not drop a single request, and you deploy many times a day.
  • Several teams ship services independently and need namespaces, quotas and RBAC.
  • Load swings enough that autoscaling saves real money.
  • Your employer or your customers already run Kubernetes, or you want to learn it: a k3s cluster on two or three small VPS is a realistic practice ground.

Moving from Compose to Kubernetes with kompose

kompose, a Kubernetes project, converts a Compose file into Kubernetes manifests. The kubernetes.io task page walks through it; the short version:

any machine with kompose and kubectl
mkdir -p k8s                         # -o writes a single file if the directory is missing
kompose convert -f compose.yaml -o k8s/
ls k8s/
kubectl apply -f k8s/
kubectl get pods,svc

For each service it writes a Deployment, a Service when the service publishes ports, and a PersistentVolumeClaim for each volume. Treat the result as a first draft:

  • depends_on has no equivalent. Kubernetes starts everything at once; use readiness probes and let the app retry its connections.
  • Volumes need a storage class. The claims stay Pending until the cluster has one that can serve them.
  • Images must come from a registry. Nodes cannot use an image that exists only on your build machine, so push what build: produced before you apply (kompose can run the build and the push for you).
  • Resources and probes are yours to add. Without requests, limits and readiness probes, the scheduler and the rolling update are working blind.

Frequently asked

Is Docker Compose enough for production?

On a single server, yes, for many applications: restart policies, healthchecks, named volumes and a reverse proxy cover what one host needs. What it cannot give you is survival of the host itself or zero-downtime updates, because containers are recreated in place. A backup that you have restored at least once on a second server covers most of that gap for a small team.

Can Kubernetes use a docker-compose.yml file?

Not directly. Kubernetes reads its own objects, such as Deployments, Services and PersistentVolumeClaims. The kompose tool converts a Compose file into those manifests with kompose convert -f compose.yaml, which makes a good first draft; depends_on, images that only exist locally, resource limits and readiness probes still need manual work.

Is k3s real Kubernetes?

Yes. k3s is a certified Kubernetes distribution packed into one binary, with a lighter default datastore (SQLite on a single server) and bundled networking, DNS, ingress and local storage. Manifests and kubectl work as on any other cluster. Its documentation lists 2 cores and 2 GB of RAM as the minimum for a server node.

Do I need Kubernetes for a single VPS?

Usually not. Kubernetes' main benefits, rescheduling when a node fails, rolling updates across machines and autoscaling, need more than one node to matter, while its control plane costs memory and attention. Docker Compose is the simpler fit for one VPS; k3s is the option if you want to learn Kubernetes or reuse existing manifests.

Related articles

All comparisons