Kubernetes is often pitched as the "next step" after Docker, but it’s more accurate to say they operate at fundamentally different levels of abstraction.
Let’s see Kubernetes orchestrating a simple web application. Imagine we have a stateless web app, my-web-app, that we want to deploy with three replicas.
First, we define our application in a Deployment YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-web-app-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-web-app
template:
metadata:
labels:
app: my-web-app
spec:
containers:
- name: my-web-app-container
image: nginx:latest
ports:
- containerPort: 80
Then, we apply this to our Kubernetes cluster:
kubectl apply -f deployment.yaml
Kubernetes now takes over. It ensures three pods, each running the nginx:latest image, are always running. If one pod crashes, Kubernetes automatically starts a new one. If we want to scale up to five replicas, we just change replicas: 3 to replicas: 5 and re-apply. Kubernetes handles the creation of two new pods.
Now, let’s expose this application externally. We create a Service YAML:
apiVersion: v1
kind: Service
metadata:
name: my-web-app-service
spec:
selector:
app: my-web-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
Applying this:
kubectl apply -f service.yaml
Kubernetes provisions a cloud provider’s load balancer (or uses an internal one if configured) and directs external traffic to one of our healthy my-web-app pods. It monitors the health of our pods and routes traffic only to those that are responsive.
This entire process – ensuring desired state, self-healing, scaling, and exposing services – is Kubernetes’ domain. Docker, on the other hand, is the tool that builds and runs the individual containers that Kubernetes manages. You use docker build to create your container image and docker run to start a single container on a single machine. Kubernetes automates the docker run (or equivalent container runtime) command across a fleet of machines.
The core problem Kubernetes solves is managing distributed applications at scale. When you have more than a few containers, or when you need high availability, automatic scaling, and self-healing, manually managing individual containers becomes an operational nightmare. Kubernetes provides a declarative API to define your desired application state, and it continuously works to achieve and maintain that state. It abstracts away the underlying infrastructure, allowing you to think about your application in terms of pods, deployments, and services rather than individual virtual machines and running processes.
Docker provides the fundamental unit of deployment: the container image and runtime. It’s how you package your application and its dependencies into a portable, isolated environment. Kubernetes then takes these container images and orchestrates their deployment, scaling, networking, and lifecycle management across a cluster of machines. You need a container runtime (like Docker or containerd) for Kubernetes to work, but Kubernetes is what makes running containers at scale manageable.
Most people focus on Kubernetes’ ability to start and stop containers, but its real power lies in its control loops. For example, the Deployment controller constantly watches the actual state of your pods against the desired state defined in the Deployment object. If it sees fewer pods than requested, or if a pod is unhealthy, it automatically initiates actions (like creating new pods or terminating unhealthy ones) to reconcile the difference. This continuous reconciliation is the engine that drives Kubernetes’ resilience and automation.
The next step is understanding how Kubernetes handles persistent storage for stateful applications.