Docker vs Kubernetes: One Request Through Both
Docker and Kubernetes are not competitors on the same layer, and treating them as an either/or is the mistake that leads teams to run a cluster they cannot operate. Docker builds an image and runs a container on one host. Kubernetes takes that same container and keeps a declared number of copies alive across many hosts, reconciling networking and rollout state as machines and Pods come and go.
This guide is for engineers who can already run docker run and now have to decide what production looks like. We follow one service — a checkout-api exposing GET /health — first as a single Docker container, then as a Kubernetes Deployment. You will trace how a request actually reaches it, watch one real crash loop and recover from it, see where the security and scaling boundaries move, and leave with a decision rule that is not based on service-count folklore. For the cluster internals in more detail see Kubernetes Request Flow; for persistence see Docker Volumes Explained.
Docker: build and run on one host
What it is. Docker turns a Dockerfile into an image (a read-only, layered filesystem with your app and its runtime) and runs that image as a container (an isolated process that shares the host kernel but has its own filesystem, network namespace, and cgroup limits). You type into the Docker client (docker build, docker run); the client sends the command to the Docker daemon, which does the real work of building layers, pulling from a registry, and starting the process.
How it runs. docker build executes each Dockerfile instruction as a cached layer, so a later code change does not reinstall dependencies. docker run starts the process, maps a host port to the container port, and attaches namespaces for isolation. The container is just a process: when it exits, it stops. Nothing restarts it, reschedules it, or moves it to another machine — that is exactly the gap Kubernetes fills.
Boundary. Docker owns packaging and single-host execution. It does not own multi-node placement, self-healing, or a stable network identity across restarts. On one reliable VM with Compose, that is often all you need.
Quick reference
- Client → daemon → container: the CLI never runs the container itself.
- Layers are cached: order the Dockerfile cheap-to-expensive to keep rebuilds fast.
- A container is a process with namespaces + cgroups, not a lightweight VM.
USER nodematters: root in a container is root on shared kernel resources.- On exit, nothing reschedules it — no health loop, no failover.
Remember this
Docker packages an app into an image and runs it as one isolated process on one host — with no built-in restart, placement, or failover.
Kubernetes: a control loop over many hosts
What it is. Kubernetes runs the same image across a cluster of machines and continuously drives the system toward a state you declare in YAML. You do not run containers directly; you tell the API server "I want 3 replicas of checkout-api:1.0.0," and controllers make reality match.
The mechanism that matters — reconciliation. This is the one idea that explains almost everything Kubernetes does. Your desired state lives in etcd. A controller watches for any difference between desired and actual state and acts to close the gap. Declare 3 replicas and only 2 are running? The Deployment/ReplicaSet controller creates one more. The scheduler picks a node with room; that node's kubelet pulls the image and starts the Pod; the container runs on containerd. Kill a Pod and the same loop notices and replaces it. You are not scripting steps — you are declaring an end state and a loop keeps enforcing it.
Boundary. Kubernetes owns desired-state orchestration: placement, self-healing, stable service identity, and coordinated rollouts. It does not replace Docker's image format (it runs the same OCI images) and it does not make an app correct — a bad image crash-loops faster and more visibly, not less.
Quick reference
- Declarative, not imperative: you state the end result; controllers reach it.
- Reconciliation loop = watch desired vs actual, act to close the gap, repeat.
- Scheduler places Pods; kubelet runs them; etcd stores the desired state.
- A Service gives a stable name/IP even as Pods are created and destroyed.
- It runs the same OCI images Docker builds — the runtime is usually containerd.
Remember this
Kubernetes is a reconciliation loop: you declare desired state, and controllers place, heal, and reroute Pods to keep actual state matching it.
How a request actually reaches a Pod
Knowing Pods run somewhere is not enough — you need to know why curl http://checkout-api/health lands on a healthy instance and never on a crashing one. Pods are mortal and their IPs change; a Service exists precisely to hide that churn behind one stable name and virtual IP (ClusterIP).
Trace one request. In-cluster DNS resolves checkout-api to the Service's ClusterIP. On each node, kube-proxy has programmed iptables/IPVS rules that rewrite that virtual IP to the real IP of one backing Pod. The set of eligible Pods is the Endpoints (EndpointSlice) list — and this is the key mechanism: a Pod appears in that list only while it passes its readinessProbe. A Pod that is still booting, or is failing /health, is removed from the list, so kube-proxy never forwards traffic to it.
Why this is the payoff of the whole model. Self-healing and rolling updates only look smooth because readiness gates the Endpoints list. During a rollout, a new Pod receives traffic after it passes readiness, and an old Pod is removed before it is killed. Get the probe wrong — point it at a path that returns 200 before dependencies are ready — and Kubernetes will happily route real users to a Pod that cannot serve them.
Quick reference
- Service ClusterIP is virtual — kube-proxy (iptables/IPVS) rewrites it to a Pod IP.
- EndpointSlice = the live list of Pods eligible to receive traffic.
- readinessProbe controls membership in that list; livenessProbe controls restarts.
- Rolling update stays smooth only because readiness gates traffic per Pod.
- A probe that lies (200 before dependencies are up) routes users to broken Pods.
Remember this
A Service routes to a Pod only while it passes its readiness probe — that gate is what makes healing and rollouts look smooth.
When it breaks: CrashLoopBackOff
The most common first production incident is not exotic: a Pod stuck in CrashLoopBackOff. This section is the one every reader should be able to run.
Trigger → symptom → root mechanism. You ship checkout-api:1.1.0, but it reads DATABASE_URL from an env var that is missing in the manifest. The process throws on startup and exits non-zero. Kubernetes' job is to keep the declared replica alive, so the kubelet restarts it — then applies an exponential backoff (roughly 10s, 20s, 40s, capped) to avoid hammering a doomed container. The symptom you see is STATUS: CrashLoopBackOff with a climbing RESTARTS count. Nothing is "broken" in Kubernetes; the control loop is doing exactly what you told it to, around an app that cannot start.
Recovery. Read the events, then the logs, then roll back — in that order. kubectl describe pod shows the reason (Error, OOMKilled, ImagePullBackOff are different root causes with different fixes). kubectl logs --previous shows the crashed process's own output. Because you deployed declaratively, recovery is one command: kubectl rollout undo returns to the last healthy ReplicaSet while you fix the manifest. Contrast the sibling failure ImagePullBackOff — that is a bad tag or registry auth, so no amount of restarting helps; you fix the image reference, not the app.
Quick reference
- CrashLoopBackOff = container keeps exiting; kubelet restarts with growing backoff.
- Order of inspection:
describe(events/reason) →logs --previous→ fix. - OOMKilled means the memory limit was too low — raise limits or fix the leak.
- ImagePullBackOff is not a crash loop: it is a bad tag or missing registry auth.
rollout undois the safe first move; debugging the new version can wait.
Remember this
CrashLoopBackOff is the control loop restarting an app that cannot start; recover by reading events then logs, and roll back with one declarative command.
What changes: security and scaling boundaries
Moving from Docker to Kubernetes does not just add machines — it moves where security and scaling decisions live, and skipping that shift is how clusters become liabilities.
Security boundary. On one Docker host, isolation is namespaces plus whatever the host firewall allows. In a cluster, identity and blast radius are explicit objects: RBAC decides which humans and ServiceAccounts can touch which resources; Secrets hold DATABASE_URL and are mounted into Pods rather than baked into the image; NetworkPolicies decide which Pods may talk to which; and a securityContext (runAsNonRoot, dropped Linux capabilities, read-only root filesystem) limits what a compromised container can do. The same USER node instinct from the Dockerfile now has a cluster-level counterpart — and forgetting it means a single exploited Pod can reach far more than one host.
Scaling boundary. Docker scales by you running more containers. Kubernetes can scale on measured conditions: a HorizontalPodAutoscaler adds replicas when a signal (CPU, memory, or a custom metric like requests-per-second) crosses a target, and the Cluster Autoscaler adds nodes when Pods cannot be scheduled. Autoscaling only works if you set resource requests/limits honestly — requests drive scheduling and HPA math; limits cap a noisy neighbor. Wrong requests give you either constant pending Pods or a cluster that scales on noise. Scaling is a workload property (does load actually vary? is the app horizontally scalable?), not a default you turn on.
Quick reference
- RBAC + ServiceAccounts scope who/what can act; default-allow is a mistake.
- Secrets and config live outside the image and mount into Pods.
- securityContext (non-root, dropped caps, read-only FS) limits blast radius.
- HPA needs resource requests; without them scheduling and autoscaling misfire.
- Cluster Autoscaler adds nodes only when Pods are genuinely unschedulable.
Remember this
Kubernetes turns security and scaling into explicit objects — RBAC, Secrets, securityContext, requests/limits, HPA — that you must set deliberately, not defaults you inherit.
Which should you use?
Choose from the operational requirement backward. Use Docker (with Compose) when one host can meet your availability and scale needs: local development, CI image builds, internal tools, and small single-server deployments. You keep the entire mental model in your head, and there is no control plane to operate.
Add an orchestrator when you can name the capability you are missing — multi-node failover, coordinated zero-downtime rollouts, autoscaling on real signals, or policy-driven placement — and you can staff its operation. Notice the trap: service count is a weak trigger. One regulated payment service may justify a cluster; twenty stateless internal workers may run happily on a PaaS. Reach for the reason, not the number.
Prefer managed before self-managed. Managed container platforms — Cloud Run, ECS/Fargate — preserve the Docker workflow (build image → push → deploy) with little or no control-plane work, and managed Kubernetes (EKS/GKE/AKS) removes the hardest operational parts while keeping the full API. Self-managing a cluster is a deliberate choice you make when portability or specific control genuinely requires it — not a rite of passage.
Quick reference
- One app, modest availability → Docker Compose or a PaaS.
- Need managed scaling without cluster APIs → Cloud Run or ECS/Fargate.
- Need K8s APIs, policy, and portability → managed cluster before self-hosting.
- Trigger is a named missing capability + ability to operate it — not service count.
- Docker stays in the picture regardless: it builds the image K8s runs.
Remember this
Use Docker until a single host cannot meet a named reliability or scaling need; adopt (preferably managed) orchestration for that specific capability — never because of service count.
Key takeaway
Docker and Kubernetes solve different layers: Docker defines and runs the container unit; Kubernetes wraps that unit in a reconciliation loop that places, heals, routes, secures, and scales it across a cluster. Everything Kubernetes appears to do 'magically' — self-healing, smooth rollouts — reduces to one control loop plus readiness-gated Services. Adopt it for a capability you can name and operate, and let Docker keep doing the packaging underneath.
Practice (30 min): Package and run the checkout-api; GET /health should succeed in Docker and later through a Kubernetes Service with three Ready Pods. Intentionally use a missing image tag, then remove a required environment variable to observe ImagePullBackOff and CrashLoopBackOff. Recover each with corrected configuration or kubectl rollout undo, and delete one healthy Pod to watch reconciliation replace it. Pass when health succeeds again, the Service routes only to Ready Pods, and you can state where the request lands and who performs recovery in both environments.
Related Articles
Explore this topic