Docker vs Kubernetes: What's Actually Different
They are not competitors. Docker is a container engine and image toolchain that builds, ships and runs containers on a single host. Kubernetes is a cluster orchestrator that schedules containers across many machines and keeps them matching a declared desired state. You build images with Docker and run them on Kubernetes. The true head-to-head is Docker Swarm vs Kubernetes.
The comparison is a category error
Docker is a container engine plus an image build and distribution toolchain, and everything it does happens on one machine. Kubernetes is a cluster orchestrator: you tell it what you want running, and it decides which of your machines runs what, then keeps fixing reality until reality matches. They sit at different layers of the same stack. The normal setup is both: docker build produces the image, Kubernetes runs it.
The confusion is earned, not stupid. For years docker was the thing that built your image and the thing Kubernetes talked to on every node, so the two names welded together in people's heads. Then Kubernetes stopped talking to Docker Engine and a wave of "Kubernetes is dropping Docker" headlines cemented the idea that they were rivals.
The genuinely apples-to-apples comparison is Docker Swarm vs Kubernetes — Swarm mode shipped inside Docker Engine 1.12 in 2016 and does the same job as Kubernetes: clustering, scaling, rolling updates, overlay networking across hosts. That is the fight Kubernetes won.
The rest of this article covers what each layer actually owns, a concept-by-concept mapping from Docker commands to Kubernetes objects, what really happened in Kubernetes 1.24, and when a single Docker host is the correct answer.
What Docker owns: build, ship, run — on one host
Docker Engine is a stack, not a single program. The docker CLI talks to the dockerd daemon, dockerd delegates the container lifecycle to containerd, and containerd invokes runc — the reference implementation of the OCI Runtime Specification — which actually spawns the process inside Linux namespaces and cgroups. ("Namespaces" and "cgroups" are the kernel features that give a process its own isolated view of the filesystem and network, and a cap on the CPU and memory it can use. That is all a container is.)
Docker donated containerd to the Cloud Native Computing Foundation, and it graduated on 28 February 2019. Remember that — it matters in the 1.24 section.
The Docker ecosystem also produced the three specifications the Open Container Initiative now maintains: the Image Spec (what an image file looks like), the Runtime Spec (how to start one), and the Distribution Spec (how registries serve them). Those specs are why an image built on your laptop runs anywhere.
For multi-container apps, Docker Compose reads a compose.yaml, creates a user-defined network for the project, and starts everything so services can find each other by service name.
Note
DNS between containers only works on user-defined networks. On Docker's default bridge, containers can only reach each other by IP address. Compose sidesteps this by creating a project network for you.
Two Docker behaviours define the ceiling. First, restart policies — no (the default), on-failure, always, unless-stopped — are enforced by the local daemon. If the container dies, the daemon restarts it. If the host dies, nothing restarts anything, because the thing responsible for restarting died too. Second, Docker has no concept of a second machine. Every command is scoped to one daemon.
What Kubernetes adds: desired state and a scheduler
docker run is an imperative instruction: start this, now. Kubernetes is declarative. You submit an object saying "I want three replicas of this image" to the kube-apiserver, which stores it in etcd, and controllers then run a loop forever: compare what exists to what was asked for, and act on the difference. Nobody tells Kubernetes to restart a crashed pod. A controller notices the count is wrong and creates one.
The control plane is kube-apiserver, etcd (the consistent key-value store holding all cluster data), kube-scheduler (picks which node a workload lands on), kube-controller-manager (runs the reconciliation loops) and optionally cloud-controller-manager. Every worker node runs kubelet (starts and watches containers), kube-proxy (wires up Service networking) and a container runtime.
The scheduling unit is not a container but a Pod: one or more containers that share a network namespace — the same IP and port space, so they reach each other over localhost — and can share volumes. The extra layer exists so a sidecar (a log shipper, a proxy) can sit beside your app as if it were on the same machine. The docs explicitly recommend you never create Pods directly; use a workload resource like a Deployment, which creates them for you.
What that buys you that Docker alone cannot:
- Self-healing across machines. A Deployment manages a ReplicaSet, which manages Pods. Lose a node, lose its Pods, and the ReplicaSet recreates them elsewhere.
- Rolling updates and rollback. The default strategy is
RollingUpdatewithmaxUnavailableandmaxSurgeboth defaulting to 25%. Kubernetes keeps 10 old ReplicaSets (revisionHistoryLimit) so you can roll back, and reports the rollout failed if it has not progressed withinprogressDeadlineSeconds, default 600. - Autoscaling. The HorizontalPodAutoscaler is a control loop in
kube-controller-managerrunning every--horizontal-pod-autoscaler-sync-period, default 15 seconds, with a scale-down stabilisation window defaulting to 300 seconds so load spikes do not cause flapping. - Cluster DNS. CoreDNS gives every Service a stable name,
<service>.<namespace>.svc.cluster.local, resolvable from any Pod on any node.
The scale guidance tells you what problem this was built for: up to 5,000 nodes, 150,000 Pods, 300,000 containers, no more than 110 Pods per node.
The honest cost: a control plane to run, etcd to back up, a CNI network plugin to choose, and dozens of object types to learn before you deploy one web app. Much like choosing between Lambda and EC2, the right answer depends on the shape of your problem, not on which technology is newer.
The mapping table
| Docker | Kubernetes |
|---|---|
docker run image |
Deployment → ReplicaSet → Pod |
docker run --restart=always |
restartPolicy: Always (the Pod default) |
docker run -p 8080:80 |
Service, plus Ingress for HTTP routing |
docker run -v data:/var/lib |
PersistentVolumeClaim + StorageClass; emptyDir for scratch |
docker run -e / --env-file |
ConfigMap and Secret |
docker run -m 512m --cpus 1 |
resources.requests and resources.limits |
docker compose up |
A set of Deployments + Services in a Namespace |
docker build |
Nothing. Kubernetes has no build step. |
Details that bite:
Restarts. restartPolicy is Always, OnFailure or Never, defaulting to Always. When a container keeps failing, the kubelet backs off exponentially: 10s, 20s, 40s, capped at 5 minutes. A container sitting in that delay shows as CrashLoopBackOff — which is a waiting state, not an error type. The timer resets once a container has run successfully for 10 minutes.
Ports. Service defaults to type ClusterIP, reachable only from inside the cluster. NodePort opens a port on every node in the range 30000–32767 (configurable with --service-node-port-range). LoadBalancer asks your cloud for a real load balancer.
Resources. requests drive scheduling — the scheduler adds up requests to decide if a Pod fits on a node. limits are enforced at runtime, and the two failure modes differ sharply: exceed a CPU limit and you are throttled; exceed a memory limit and the kernel OOM killer terminates the container. CPU is measured in cores, expressible in millicpu, so 500m is half a core and 1m is the minimum.
Common mistake
Setting
requestsfar below actual usage to pack more Pods onto a node. The scheduler believes the request, fills the node, and then memory limits get hit and containers get OOM-killed under load — on the node you just overcommitted.
Config. ConfigMaps and Secrets are both capped at 1 MiB, because that is etcd's object size limit. Secret data is base64-encoded, not encrypted — encryption at rest is a separate API server configuration. Base64 is encoding, not security; anyone who can read the Secret can read the value. If you have thought about how AWS IAM scopes permissions, the same instinct applies: access control is what protects a Secret, not its encoding.
What actually happened in Kubernetes 1.24
The kubelet talks to container runtimes over the Container Runtime Interface, a gRPC API introduced as alpha in Kubernetes 1.5 in December 2016. Docker Engine does not implement CRI. So the kubelet shipped a built-in adapter, dockershim, whose only job was translating CRI calls into Docker Engine calls — extra code the Kubernetes project had to maintain for one runtime.
It was deprecated in Kubernetes 1.20 (announced 2 December 2020) and removed from the kubelet in 1.24, released May 2022.
Here is the part that got misreported: your images are fine. Images built with docker build conform to the OCI Image Spec, and containerd and CRI-O run them unchanged. Nothing in your Dockerfile changed. And recall that Docker Engine already uses containerd underneath — the layer that got removed was the shim, not the runtime.
What did change is what node operators see. After migrating off dockershim, docker ps and docker inspect on a node no longer show Kubernetes containers, because Docker Engine is no longer the thing running them — you use crictl instead. Docker-specific logging drivers stop applying, and the pattern of mounting /var/run/docker.sock into a Pod to build images inside the cluster stops working; Kaniko, BuildKit or Buildah replace it. If you must keep Docker Engine as the runtime, cri-dockerd is an adapter maintained by Mirantis and Docker that does what dockershim did, out of tree.
Interview tip
"Why did Kubernetes remove Docker support?" is a common interview question, and the expected answer is the distinction between a runtime and an image format. Kubernetes removed the dockershim adapter for Docker Engine as a node runtime. It never stopped running Docker-built images, because those are OCI images.
When Docker alone is the right answer
One host, one app, no high-availability requirement, and a few seconds of downtime during deploys is acceptable? Docker and Compose, and Kubernetes is pure overhead — a control plane and an object vocabulary bought to solve a problem you do not have. CI runners, local development, and build tooling are Docker/BuildKit territory regardless of where you deploy.
You cross into orchestrator territory when you need any of: zero-downtime rolling deploys, workloads that survive a node dying, autoscaling, more than one machine, or role-based access control over who can deploy what.
Between the extremes there is more than "learn all of Kubernetes." Managed control planes like EKS, GKE and AKS remove the etcd-and-upgrades burden. K3s and k0s are lightweight distributions for small fleets. kind and minikube give you a throwaway local cluster. And Docker Swarm is still a reasonable fit for very small clusters — note its managers use the Raft consensus algorithm, so an N-manager swarm tolerates losing (N−1)/2 managers and you want an odd number of them.
One more asymmetry worth internalising, in the same spirit as knowing exactly what triggers a Kafka consumer group rebalance before you tune anything: Kubernetes never builds images. It pulls them from a registry, with a default imagePullPolicy of IfNotPresent — except for the :latest tag or no tag at all, which default to Always. Build stays Docker's job forever.
So the question was never "Docker or Kubernetes". It is "do I have a one-machine problem or a many-machine problem" — and either way, you are shipping an OCI image that Docker built.
Frequently asked questions
- Do I need to install Docker to use Kubernetes?
- No. Kubernetes nodes need a CRI-compliant runtime such as containerd or CRI-O, not Docker Engine. You will still likely want Docker on your development machine and CI runners to build images, since Kubernetes has no build capability at all — it only pulls images from a registry.
- Does Kubernetes still run images built with docker build?
- Yes, unchanged. Images produced by `docker build` conform to the OCI Image Specification, and containerd, CRI-O and any other CRI-compliant runtime run them without modification. The dockershim removal in Kubernetes 1.24 affected only how the kubelet talks to a runtime on the node, not the image format.
- What is the difference between a Pod and a container?
- A container is one isolated process with its own filesystem. A Pod is Kubernetes' smallest deployable unit and wraps one or more containers that share a network namespace — the same IP and port space, so they can talk over localhost — and can share volumes. Most Pods hold a single application container; extra containers are usually sidecars like log shippers or proxies.
- Can I convert my docker-compose.yaml into Kubernetes manifests?
- Broadly, each Compose service becomes a Deployment plus a Service, published ports become Service or Ingress definitions, volumes become PersistentVolumeClaims, and environment variables become ConfigMaps or Secrets. The mapping is mechanical but not exact, because Compose is a file format executed by a CLI while Kubernetes objects are continuously reconciled by controllers.
- Why did Docker Swarm lose to Kubernetes if it was simpler?
- Swarm mode, built into Docker Engine since 1.12 in 2016, does cover clustering, scaling, rolling updates and overlay networking, and it remains genuinely easier to stand up. Kubernetes won on ecosystem breadth and extensibility — the controller and CRD model let vendors and cloud providers build on it, which Swarm's fixed feature set did not invite. Swarm is still a defensible choice for very small fleets.
- What does CrashLoopBackOff actually mean?
- It means a container keeps exiting and the kubelet is waiting before trying again. The delay grows exponentially from 10 seconds, doubling to a maximum of 5 minutes, and resets once the container has run successfully for 10 minutes. CrashLoopBackOff is the waiting state, not the cause — check the container logs and its exit code to find out why it is dying.
References
- Kubernetes Components (Control Plane and Node components)Kubernetes
- PodsKubernetes
- DeploymentsKubernetes
- ServiceKubernetes
- Pod Lifecycle (restart policy and container restart backoff)Kubernetes
- Resource Management for Pods and ContainersKubernetes
- Considerations for large clustersKubernetes
- Updated: Dockershim Removal FAQKubernetes Blog
- Kubernetes is Moving on From Dockershim: Commitments and Next StepsKubernetes Blog
- Kubernetes 1.24: Stargazer (release announcement)Kubernetes Blog
- Container Runtime Interface (CRI)Kubernetes
- Introducing Container Runtime Interface (CRI) in KubernetesKubernetes Blog
- ConfigMapsKubernetes
- SecretsKubernetes
- Horizontal Pod AutoscalingKubernetes
- Persistent VolumesKubernetes
- DNS for Services and PodsKubernetes
- Images (image pull policy)Kubernetes
- Docker Engine overviewDocker
- Docker Compose overviewDocker
- Swarm mode overviewDocker
- Bridge network driverDocker
- Start containers automatically (restart policies)Docker
- OCI Image Format SpecificationOpen Container Initiative
- OCI Runtime SpecificationOpen Container Initiative
- Open Container Initiative — specifications overviewOpen Container Initiative
- opencontainers/runcrunc
- containerd — an industry-standard container runtimecontainerd
- CNCF announces containerd graduationCNCF
- Migrating from dockershimKubernetes