Docker Compose vs Kubernetes: which one?
Compose runs a multi-container app on one host. Kubernetes schedules across machines. Which one your VPS needs, and the four things only a cluster buys.
Docker Compose vs Kubernetes: what the question really asks
Docker Compose vs Kubernetes looks like a choice between two tools that do the same job, and it is not. Compose describes a multi-container application on one host: which images run, which ports they publish, which volumes they mount, and which network they share. Kubernetes schedules containers across a set of machines and keeps them running there, moving work when a machine goes away. Compose is a file format plus a command that reads it. Kubernetes is a distributed control loop with its own API server and its own network model.
That difference answers the question for most readers. If you run one VPS, there is nothing for Kubernetes to schedule across, so the features that pay for its complexity are switched off before you begin.
Docker draws the same boundary on its own production page: "The easiest way to deploy an application is to run it on a single server, similar to how you would run your development environment." Compose is a single-host tool, and a single host is a supported place to run real software.
What Docker Compose actually gives you
A Compose file is a description of the end state on one machine. the Compose file layout and the commands that drive it covers the syntax in full. The part that matters for this comparison is the scope.
services:
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
web:
image: caddy:2
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:80"
volumes:
db_data:docker compose up -d reads that file and makes the host match it. docker compose ps shows what is running, and docker compose logs -f web shows why a container is unhappy. Nothing sits in the background watching for drift, because the reconciliation happens when you run the command. That is the single most important behavioural difference from Kubernetes, which reconciles continuously whether you are looking or not.
The four things you only get from an orchestrator
Rescheduling on node failure. restart: unless-stopped restarts a container when it exits, and again when the Docker daemon starts. It cannot help when the machine is gone, because the process that would do the restarting died with the machine. Kubernetes keeps the control plane separate from the workload: a node stops sending heartbeats, the node controller marks it NotReady, and its pods are recreated after the eviction timeout. This only works when a surviving node exists to take them.
Rolling updates gated on health. Change an image tag and run docker compose up -d, and Compose stops the old container, then starts the new one. There is a gap, and nothing checks that the replacement serves traffic before the original is removed. docker compose up -d --wait waits for the health check to pass before it returns, which tells you the deploy worked, but the old container is already gone by then. A Kubernetes Deployment starts new pods, waits for the readiness probe, and only then removes old pods, with maxSurge and maxUnavailable setting how much overlap you allow. kubectl rollout undo deployment/web puts the previous version back.
Horizontal scaling across machines. docker compose up -d --scale web=3 runs three copies of one service on the one host, and it cannot work for a service that publishes a fixed host port, because two containers cannot both own port 8080. Three copies on one VPS share one CPU allowance, one network interface, one disk and one kernel. More copies absorb more concurrent requests. They cannot add hardware. Kubernetes places replicas on different nodes, and the scheduler tracks what each node has left to give.
Cluster-wide service discovery. Compose puts every service on a project network and gives it a DNS name, so db resolves from inside the web container. That name means something on that host and nowhere else. A Kubernetes Service is a stable name and virtual IP that resolves from any node, with traffic rewritten to a healthy pod wherever it landed. On one host, the Compose network already does this job.
What Compose does that people assume needs Kubernetes
Most of the "Kubernetes has it, Compose does not" list is wrong. These are all Compose features, and together they cover single-host operations:
- Automatic start after a reboot, through the restart policy and the Docker service, which is the whole subject of making a Compose stack come back after a reboot.
- Memory and CPU ceilings per service, so one container cannot take the box down. Setting memory limits in a Compose file shows the syntax and what the kernel does when a container passes its limit.
- TLS certificates and hostname routing for many apps behind one IP, using Traefik as a reverse proxy in front of a Compose stack.
- Startup ordering that waits for real readiness, through
depends_onwithcondition: service_healthy, as in the file above. - Secrets read from files instead of being written into the Compose file, plus per-environment overrides through a second Compose file.
Compose is also not being retired. The v1 Python tool was retired and the v2 Go plugin replaced it, which is where the confusion starts: what was actually deprecated in Docker Compose separates the two. If Docker Engine itself is new on this server, begin with installing Docker on a VPS before either side of this comparison matters.
Does single-node k3s buy resilience?
No, and that is the honest answer. k3s is a small Kubernetes distribution that installs with one command:
curl -sfL https://get.k3s.io | sh -
sudo k3s kubectl get nodesThe kubeconfig is written to /etc/rancher/k3s/k3s.yaml and holds cluster-admin credentials, so it stays mode 600 and never gets copied to a shared folder. The k3s documentation lists 2 CPU cores and 2 GB of RAM as the minimum for a server node as of September 2026, and that is the baseline for k3s and its own components, before your workloads request anything.
On one node you do get the Kubernetes API, and that is a real thing to want. Software published only as a Helm chart or an operator installs the way its authors intended. Manifests are reconciled continuously, so a pod you delete by accident comes back on its own. Readiness probes gate rollouts, kubectl rollout undo reverses one, and RBAC lets a second person deploy without receiving root on the box.
What you do not get is the property people install Kubernetes for. One node means the workload dies when the node dies, exactly as with Compose. You also take on a container network plugin, an ingress controller, a storage provisioner whose volumes live on that same single disk, certificate rotation inside the cluster, and a datastore that now needs its own backup. Running single-node k3s on a VPS walks through the install and the first workload, and hardening that single-node cluster covers the surfaces the default install leaves open. Both are the honest next step if you decide you want the API. Neither turns one server into two.
How to choose, in four questions
- Do you have more than one machine right now, and does losing one need to be survivable? One machine means an orchestrator has nothing to orchestrate.
- Is the software you want published only as a Helm chart or an operator, with no maintained Compose file? Forcing a chart into Compose by hand can cost more than the cluster does.
- Does someone other than you need to deploy, with roles and an audit trail? Compose has no answer beyond SSH access, which is all or nothing.
- Are you learning Kubernetes because a job will ask for it? That is a good reason on its own, and it does not need to be dressed up as an availability decision.
Ten services on one box is a Compose problem. It stays a Compose problem until one of those four answers changes.
Migrating from Compose to Kubernetes manifests
kompose converts a Compose file into Kubernetes objects. Release v1.38.0 is current as of September 2026, so check the project's releases page before you pin that version.
curl -L https://github.com/kubernetes/kompose/releases/download/v1.38.0/kompose-linux-amd64 -o kompose
chmod +x kompose
sudo mv ./kompose /usr/local/bin/kompose
kompose convert -f docker-compose.ymlThat writes one Deployment and one Service per Compose service, carrying over the image, the command, the environment variables and the ports. Read the output before applying any of it, because four things do not survive the trip:
- Named volumes become PersistentVolumeClaims that need a StorageClass which exists on the cluster. Without one, the pod stays in
Pending, andkubectl describe pod <name>shows the reason underEvents. - Published host ports become a Service, not an Ingress. Nothing outside the cluster reaches the app until you write the Ingress and have a controller running to serve it.
depends_onhas no equivalent. Ordering becomes readiness probes plus an application that retries its database connection instead of exiting on the first failure.restart: unless-stoppeddisappears, because a Deployment always restarts its pods. A one-shot task that relied onrestart: "no"needs a Job object instead.
Treat the generated YAML as a first draft to learn from. People who finish this move usually end up writing the manifests by hand, or a small Helm chart, because the converted objects still encode single-host assumptions the cluster does not share.
Most self-hosters never make this move, and nothing is wrong with that. A Compose file that has run the same ten services for two years is a working system. The work that pays off on that system is backing up and upgrading the Compose stack safely, not converting it into manifests for a cluster you do not have.
What actually goes wrong on each side
Compose, during a deploy. The gap between stop and start is real downtime, usually a few seconds, and much longer when the image pull happens inside that window. Run docker compose pull first, then docker compose up -d, so the slow part finishes while the old container is still serving.
Compose, after a reboot. A service with no restart policy stays down, and nothing tells you. Reboot the VPS on purpose once, then run docker compose ps, so you learn this on your schedule instead of from a user.
k3s, when the disk fills. The kubelet evicts pods under disk pressure, and pods you never touched move to Evicted. One node makes this worse, because there is nowhere else for those pods to be placed.
k3s, on a small VPS. The control plane competes with your workloads for the same 2 GB of RAM. Pods sit in Pending because no node can satisfy their resource requests, and kubectl describe pod <name> names the reason in its Events list. On a single node the fix is always the same: give the box more memory, or run fewer things.
FAQ
Is Docker Compose good enough for production?
Yes, on one host. Docker's own production guidance describes running an application on a single server as the easiest way to deploy it, and a Compose file with restart policies, health checks, resource limits and file-based secrets covers what a single-host deployment needs. What Compose cannot do is survive the loss of that host. If a few minutes of downtime while you rebuild a VPS from backups is acceptable, Compose is production ready for your case. If it is not, the fix is a second machine first, and an orchestrator only after that.
Do I need Kubernetes to run ten services on one VPS?
No. Ten services on one host is the exact problem Compose was built for: one file and one command. Kubernetes adds an API server, a scheduler, a network plugin, an ingress controller and a storage layer, and each of those is something that can break at three in the morning. None of them makes ten containers on one box more available than a restart policy already does, because the box is still the thing that fails.
Can I convert a docker-compose.yml file to Kubernetes manifests?
kompose convert -f docker-compose.yml generates a Deployment and a Service for each Compose service, with the image, environment variables and ports carried across. It cannot invent a StorageClass for your named volumes, an Ingress for your published ports, or startup ordering to replace depends_on. Read the generated YAML as a draft and finish it by hand. Many people find that writing the manifests from scratch is faster than repairing the converted ones.
When is single-node k3s worth it on a VPS?
When you want the Kubernetes API rather than high availability. Two reasons stand up: the software you need ships only as a Helm chart or an operator, and learning Kubernetes for a job that will ask for it. One node gives you continuous reconciliation, health-gated rollouts, RBAC and the whole chart ecosystem. It does not give you rescheduling, because there is nowhere to reschedule to, and it spends 2 GB of RAM and a lot of your attention to tell you so.