Docker and Kubernetes are the two most talked-about tools in modern infrastructure — but they're often misunderstood as competing alternatives. They're not. Docker creates containers; Kubernetes orchestrates them. Most production systems use both. This guide explains exactly what each tool does, how they differ, and when you need each one.
At a glance
| Docker | Kubernetes | |
|---|---|---|
| Primary purpose | Build, ship, and run containers | Orchestrate containers at scale |
| Created by | Docker Inc (2013) | Google, donated to CNCF (2014) |
| Scope | Single host | Multi-node cluster |
| Scaling | Manual (docker run more copies) |
Automatic (HPA, VPA) |
| Self-healing | No (restart policy only) | Yes (liveness/readiness probes) |
| Load balancing | Basic (Compose ports) | Built-in Services + Ingress |
| Configuration | Dockerfile, docker-compose.yml | YAML manifests (Deployment, Service, etc.) |
| Learning curve | Low–Medium | High |
| Typical use | Development, single-server apps | Production, microservices, high-availability |
| Replaces the other? | No — Docker runs inside K8s nodes | No — K8s needs a container runtime |
What is Docker?
Docker is a containerisation platform. It lets you package an application and all its dependencies (runtime, libraries, config) into a single portable unit called a container. Containers are isolated processes that share the host OS kernel — lighter than VMs, yet consistent across environments.
Core Docker concepts
Dockerfile → build → Image → run → Container
↑
Docker Hub / Registry
Dockerfile — instructions to build an image:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build and run:
docker build -t myapp:1.0 .
docker run -d -p 3000:3000 --name myapp myapp:1.0
docker logs myapp
docker exec -it myapp sh
Docker Compose — multi-container on one host
For local development and simple single-server deployments, Docker Compose defines a multi-container application in one YAML file:
# docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: user
POSTGRES_DB: mydb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d # start
docker compose ps # status
docker compose logs -f api # logs
docker compose down # stop
Docker Compose is great for local dev and small single-server deployments. When you need multi-host, auto-scaling, and self-healing — that's when Kubernetes enters.
What is Kubernetes?
Kubernetes (K8s) is a container orchestration platform. It manages the lifecycle of containers across a cluster of machines — scheduling, scaling, healing, load balancing, rolling updates, and secret management.
Core Kubernetes concepts
| Object | Purpose |
|---|---|
| Pod | Smallest deployable unit — one or more containers |
| Deployment | Declares desired state (replicas, image, update strategy) |
| Service | Stable network endpoint for pods (ClusterIP / NodePort / LoadBalancer) |
| Ingress | HTTP/HTTPS routing to Services |
| ConfigMap | Non-sensitive configuration |
| Secret | Sensitive data (base64-encoded, encrypted at rest) |
| Namespace | Virtual cluster for isolation |
| PersistentVolume | Durable storage that outlives a pod |
| HorizontalPodAutoscaler | Auto-scale pods based on CPU/memory |
| Node | Physical or virtual machine in the cluster |
Kubernetes architecture
Control Plane Worker Nodes
┌────────────────────┐ ┌──────────────────────┐
│ API Server │◄───────►│ kubelet │
│ Scheduler │ │ kube-proxy │
│ Controller Mgr │ │ Container Runtime │
│ etcd (state) │ │ (containerd) │
└────────────────────┘ │ Pods → Containers │
└──────────────────────┘
Basic Kubernetes manifest
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:1.0
ports:
- containerPort: 3000
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
---
apiVersion: v1
kind: Service
metadata:
name: myapp-svc
namespace: production
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-svc
port:
number: 80
kubectl apply -f deployment.yaml
kubectl get pods -n production
kubectl rollout status deployment/myapp -n production
kubectl scale deployment myapp --replicas=5 -n production
Docker and Kubernetes work together
A common misconception: "Docker vs Kubernetes" implies you choose one. In practice, you use both:
Developer writes Dockerfile
↓
docker build → image → pushed to registry (Docker Hub / ECR / GCR)
↓
Kubernetes pulls the image from the registry
↓
Kubernetes runs it inside containers on worker nodes
↓
kubelet uses a container runtime (containerd / CRI-O) to run the containers
Note: Kubernetes deprecated the Docker daemon (dockershim) in 2020. Kubernetes now uses containerd or CRI-O directly. The Docker image format (OCI) is still supported — images you build with
docker buildrun perfectly on Kubernetes.
Docker Compose vs Kubernetes
This is the more practical comparison for most developers deciding what to use:
| Docker Compose | Kubernetes | |
|---|---|---|
| Multi-container | Yes | Yes |
| Multi-host | No (single machine) | Yes (cluster) |
| Auto-scaling | No | Yes (HPA) |
| Self-healing | No | Yes |
| Rolling updates | No | Yes (zero-downtime) |
| Load balancing | Basic port mapping | Services + Ingress |
| Secret management | .env files / Docker Secrets | Kubernetes Secrets + external (Vault, AWS SM) |
| Health checks | healthcheck directive | liveness + readiness probes |
| Config | docker-compose.yml | Multiple YAML manifests or Helm charts |
| Learning curve | Low | High |
| Setup time | Minutes | Hours–Days |
| Best for | Local dev, single-server | Production, microservices |
| Storage | Named volumes | PersistentVolumeClaims |
| Network | Bridge network per project | Pod CIDR, Services, NetworkPolicies |
| Rolling back | Manual | kubectl rollout undo |
Docker Swarm vs Kubernetes
Docker Swarm was Docker's built-in orchestrator (before K8s won the container wars). You may encounter it on older systems:
| Docker Swarm | Kubernetes | |
|---|---|---|
| Setup | Very easy (docker swarm init) |
Complex (kubeadm, managed K8s) |
| API | Docker Compose v3 syntax | Kubernetes API |
| Scaling | Yes | Yes (+ auto-scaling) |
| Self-healing | Yes (restart on failure) | Yes (advanced probes) |
| Rolling updates | Yes | Yes |
| Community | Declining | Huge (CNCF ecosystem) |
| Managed cloud | None (DIY) | EKS, GKE, AKS, DOKS, etc. |
| Extensibility | Limited | Huge (operators, CRDs, Helm) |
| Verdict | Use for simple clusters you manage yourself | Industry standard for production |
Docker Swarm is largely considered legacy. New projects should use Kubernetes or a managed serverless platform (AWS Fargate, Cloud Run).
Docker vs Kubernetes: feature deep-dive
Scaling
Docker (manual):
# Run 3 instances manually — no coordination
docker run -d -p 3001:3000 myapp:1.0
docker run -d -p 3002:3000 myapp:1.0
docker run -d -p 3003:3000 myapp:1.0
# You manage load balancing yourself
Kubernetes (automatic):
# HorizontalPodAutoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
K8s automatically adds/removes pods as CPU load changes.
Rolling updates
Docker Compose:
# All containers restart at once — downtime
docker compose up -d --force-recreate
Kubernetes (zero-downtime):
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # allow 1 extra pod during update
maxUnavailable: 0 # never kill a pod before new one is ready
kubectl set image deployment/myapp myapp=myapp:2.0
kubectl rollout status deployment/myapp # watch progress
kubectl rollout undo deployment/myapp # instant rollback
Self-healing
Docker: if a container crashes, it respects restart: always — but if it's unhealthy (returns 500 but is still running), Docker does nothing.
Kubernetes:
livenessProbe: # kill + restart pod if this fails
httpGet:
path: /health
port: 3000
failureThreshold: 3
periodSeconds: 10
readinessProbe: # remove pod from load balancer if this fails
httpGet:
path: /ready
port: 3000
failureThreshold: 2
periodSeconds: 5
K8s continuously monitors and replaces unhealthy pods. If a node dies, K8s reschedules all its pods onto healthy nodes.
Networking
| Docker | Kubernetes | |
|---|---|---|
| Container-to-container | Same network → hostname | Same pod → localhost; different pod → Service DNS |
| Service discovery | Container names | CoreDNS (myapp-svc.namespace.svc.cluster.local) |
| External access | -p hostPort:containerPort |
NodePort / LoadBalancer / Ingress |
| Network policies | Docker networks (isolation) | NetworkPolicy (fine-grained L3/L4 rules) |
| Inter-cluster | Not built-in | Service mesh (Istio, Linkerd) |
Storage
Docker:
volumes:
- ./data:/app/data # bind mount
- pgdata:/var/lib/postgresql # named volume
Kubernetes:
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myapp-pvc
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
storageClassName: gp3 # EBS on AWS
# Mount in pod
volumes:
- name: data
persistentVolumeClaim:
claimName: myapp-pvc
K8s PVCs are decoupled from pods — storage survives pod restarts/rescheduling. StorageClasses enable dynamic provisioning (AWS EBS, GCP Persistent Disk, Azure Disk, etc.).
When to use Docker without Kubernetes
Docker alone (or Docker Compose) is the right choice when:
| Scenario | Why Docker is enough |
|---|---|
| Local development | Fast, familiar, matches prod images |
| Single-server app | No need for multi-host orchestration |
| Small team / startup MVP | K8s overhead not justified |
| Simple background jobs | docker run + restart: always is fine |
| Serverless hosting (Railway, Render, Fly.io) | Platform handles orchestration |
| CI/CD runners | docker build + push to registry |
| Personal projects | Compose is massively simpler |
When to add Kubernetes
Add Kubernetes when you have:
| Need | What K8s provides |
|---|---|
| Multiple services (microservices) | Unified orchestration, service mesh |
| High availability (no single point of failure) | Multi-node, auto-healing |
| Auto-scaling (traffic spikes) | HPA/VPA/KEDA |
| Zero-downtime deployments | Rolling updates, blue/green |
| Multiple environments (dev/staging/prod) | Namespaces, Helm values |
| Team of 5+ engineers | GitOps, RBAC, audit logs |
| Compliance requirements | NetworkPolicies, PodSecurity, Secrets encryption |
| GPU workloads / ML | Node selectors, resource limits |
Full comparison
| Dimension | Docker (+ Compose) | Kubernetes |
|---|---|---|
| Purpose | Containerise apps | Orchestrate containers |
| Minimum requirement | 1 host | 1 master + 1 worker (or managed) |
| HA / failover | No | Yes |
| Auto-scaling | No | Yes (HPA, VPA, KEDA) |
| Rolling updates | No (Compose) | Yes |
| Rollback | Manual | kubectl rollout undo |
| Service discovery | Container hostname | CoreDNS + Services |
| Load balancing | Host ports | Services + Ingress |
| Config/secrets | .env, Docker Secrets | ConfigMap, Secrets, Vault |
| Storage | Volumes (local) | PVC + StorageClass (cloud) |
| Monitoring | docker stats, cAdvisor |
Prometheus, Grafana, Datadog |
| Network policies | Docker networks | NetworkPolicy (fine-grained) |
| Multi-tenancy | No | Namespaces + RBAC |
| GPU support | --gpus flag |
Resource limits + node selectors |
| Managed cloud | No (DIY) | EKS, GKE, AKS, DOKS, etc. |
| Ecosystem | docker-compose, buildx | Helm, Argo CD, Istio, Knative... |
| Operational complexity | Low | High |
| Cost (small app) | Low | Higher (control plane + nodes) |
| Cost (large scale) | Higher (manual ops) | Lower (automation pays off) |
| Learning time | Days | Weeks–Months |
Managed Kubernetes options
You don't have to manage the K8s control plane yourself:
| Provider | Service | Notable feature |
|---|---|---|
| AWS | EKS | Deep AWS integration (IAM, VPC, ELB) |
| Google Cloud | GKE | Autopilot mode — fully managed nodes |
| Azure | AKS | Free control plane |
| DigitalOcean | DOKS | Simple, affordable |
| Civo | K3s-based | Fast spin-up, cheap |
| Linode | LKE | Low cost |
Common mistakes
| Mistake | Why it's wrong | Fix |
|---|---|---|
| Using Kubernetes for a simple CRUD app | 90% of the complexity, 10% of the benefit | Use Docker Compose + managed PaaS |
| Not setting resource requests/limits | Pods compete for CPU/RAM, node dies | Always set requests and limits |
| Running as root inside containers | Security risk | USER 1000 in Dockerfile |
| Storing secrets in env vars / ConfigMaps | ConfigMaps are plaintext | Use Secrets + external secret manager |
| One big container per service | Defeats containerisation | One process per container |
| Ignoring liveness/readiness probes | K8s doesn't know your app is unhealthy | Always define both probes |
Using latest tag in production |
Can't roll back, inconsistent | Always pin image tags (myapp:1.2.3) |
| No resource quotas on namespaces | One team starves the cluster | Set ResourceQuota per namespace |
Docker vs Kubernetes vs serverless
| Docker/Compose | Kubernetes | Serverless (Lambda, Cloud Run) | |
|---|---|---|---|
| Ops overhead | Low | High | Minimal |
| Cold start | None | None | Yes (ms–s) |
| Always-on cost | Yes | Yes | Pay-per-request |
| Long-running jobs | Easy | Easy | Limited (15 min Lambda) |
| Auto-scaling to zero | No | With KEDA/Knative | Yes |
| Best for | Simple apps, local dev | Microservices, HA | Event-driven, bursty workloads |
FAQ
Q: Is Docker required to use Kubernetes? No. Kubernetes uses any OCI-compatible container runtime (containerd, CRI-O). You still use Docker to build images, but the Docker daemon isn't running on K8s nodes in modern clusters.
Q: Can I replace Kubernetes with Docker Compose in production? For simple single-server apps, yes — Docker Compose is a valid production choice. But Compose can't span multiple machines, auto-heal failing containers (beyond restart policy), or auto-scale. For high-availability production systems, K8s (or a managed alternative like ECS/Fargate) is the right choice.
Q: When does Kubernetes stop being worth it? For apps that receive predictable, moderate traffic and run on a single server, K8s overhead (complexity, cost, learning curve) often outweighs benefits. Use it when you genuinely need multi-node HA, auto-scaling, or are running many microservices.
Q: What is Helm and do I need it? Helm is the "package manager" for Kubernetes — it templates and versions YAML manifests. For anything beyond a personal project, Helm (or Kustomize) is essential for managing configuration across environments.
Q: Docker Swarm vs Kubernetes — should I ever use Swarm? Only if you have existing Swarm infrastructure and can't migrate. All new projects should use Kubernetes or a managed container service. Docker Swarm is effectively in maintenance mode.
Q: What's the minimal Kubernetes setup for a small team? Start with a managed cluster (GKE Autopilot, DOKS, or AKS with free control plane). Use namespaces for dev/staging/prod. Add ArgoCD for GitOps. This keeps ops overhead low while giving you production-grade orchestration.