Docker · Lesson 11 of 11
From Compose to Kubernetes
Learn when to move from Docker Compose to Kubernetes and translate a Compose service into Deployments, Services, ConfigMaps and Secrets with kubectl.
- Advanced
- 18 min read
- 4 objectives
Before this lessonLesson 10: Build, Push and Deploy with CI
What you will learn
- Explain what Kubernetes adds on top of Docker
- Map Compose concepts to Pods, Deployments and Services
- Deploy and scale an app on a local cluster with kubectl
- Perform a rolling update and a rollback
Your Progress
0 of 11 lessons 0%
- Lessons0 / 11
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
Docker Compose is excellent at running a stack on one machine. Once you need several machines, zero-downtime rollouts, automatic restarts when a server dies, or scaling on load, you need an orchestrator. Kubernetes (often written K8s) is the industry standard, offered as a managed service by every major cloud (EKS, GKE, AKS).
Kubernetes looks intimidating because it has a lot of vocabulary. The good news: it runs the exact same container images you have been building. This lesson maps what you already know from Compose onto Kubernetes, so you can read and write basic manifests.
Do you actually need Kubernetes?
Honestly, many apps do not. A single VM with Compose, or a managed container service like Cloud Run or ECS, is simpler and cheaper for a handful of services. Kubernetes starts to pay off when you have many services, several teams, multiple nodes for reliability, or need its ecosystem (autoscaling, service mesh, operators). It helps to understand it either way, since you will meet it in most larger companies.
The core idea: desired state
With Docker you give commands: run this, stop that. With Kubernetes you declare the state you want in YAML ("three copies of api:1.4.2 should be running") and submit it to the cluster. Controllers constantly compare reality to that declaration and fix any difference. If a node dies and takes a copy with it, Kubernetes starts a replacement elsewhere without anyone being paged.
Compose to Kubernetes, concept by concept
- Container stays a container, from the same image.
- Pod: the smallest unit Kubernetes runs, one (sometimes a few tightly coupled) containers sharing a network address. You rarely create Pods directly.
- Deployment is roughly a Compose service: which image, how many replicas, how to roll out updates.
- Service gives Pods a stable name and IP and load-balances between them. This replaces Compose's service-name DNS.
- ConfigMap and Secret replace
environment:,env_file:andsecrets:. - PersistentVolumeClaim replaces named volumes.
- Ingress (or the newer Gateway API) replaces published ports for HTTP traffic from outside.
A local cluster
Docker Desktop can enable a single-node Kubernetes cluster in its settings. On Linux, kind (Kubernetes in Docker) runs a cluster inside containers. Either way, you talk to it with kubectl:
kind create cluster --name stackcone
kubectl get nodesNAME STATUS ROLES AGE VERSION stackcone-control-plane Ready control-plane 48s v1.34.0
Translating a Compose service
Here is a small Compose service from earlier lessons:
services:
api:
image: ghcr.io/stackcone/api:1.4.2
ports:
- "8000:8000"
environment:
LOG_LEVEL: info
DATABASE_URL: postgresql://app:secret@db:5432/stackconeThe Kubernetes equivalent is more verbose, but each part maps cleanly. Put these in k8s/api.yaml (multiple documents are separated by ---):
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: info
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_URL: postgresql://app:secret@db:5432/stackcone
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/stackcone/api:1.4.2
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
readinessProbe:
httpGet:
path: /health
port: 8000
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 512Mi }
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8000The glue is labels. The Deployment stamps app: api on every Pod it creates, and the Service's selector sends traffic to any Pod with that label. Other Pods in the cluster can now reach the app at http://api, just like a Compose service name.
Apply, watch and reach the app
kubectl apply -f k8s/api.yaml
kubectl get deployments,pods,services -l app=apiconfigmap/api-config created secret/api-secrets created deployment.apps/api created service/api created NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/api 3/3 3 3 21s NAME READY STATUS RESTARTS AGE pod/api-6c9f7d8b5d-4xkq2 1/1 Running 0 21s pod/api-6c9f7d8b5d-9mzt7 1/1 Running 0 21s pod/api-6c9f7d8b5d-lp8wc 1/1 Running 0 21s
(The Service does not carry the app label itself, so it is not listed by the -l filter; kubectl get svc api shows it.) To reach it from your laptop without an Ingress, forward a port:
kubectl port-forward service/api 8080:80
# in another terminal:
curl -s localhost:8080/health{"status":"ok","version":"1.4.2"}The debugging skills from the debugging lesson carry over directly: kubectl logs, kubectl exec -it, and kubectl describe pod (which shows events like image pull errors and OOM kills).
Scaling, rolling updates and rollbacks
Delete a Pod and watch Kubernetes replace it. Scaling is one command:
kubectl delete pod api-6c9f7d8b5d-4xkq2
kubectl scale deployment/api --replicas=5
kubectl get pods -l app=apipod "api-6c9f7d8b5d-4xkq2" deleted deployment.apps/api scaled NAME READY STATUS RESTARTS AGE api-6c9f7d8b5d-9mzt7 1/1 Running 0 3m2s api-6c9f7d8b5d-lp8wc 1/1 Running 0 3m2s api-6c9f7d8b5d-r2hvn 1/1 Running 0 6s api-6c9f7d8b5d-t7c4j 1/1 Running 0 2s api-6c9f7d8b5d-x5bnq 1/1 Running 0 2s
Deploying a new version is changing the image tag. Kubernetes replaces Pods gradually, only sending traffic to new ones once their readiness probe passes, so users see no downtime:
kubectl set image deployment/api api=ghcr.io/stackcone/api:1.5.0
kubectl rollout status deployment/api
# something is wrong? go back
kubectl rollout undo deployment/apiWaiting for deployment "api" rollout to finish: 2 out of 5 new replicas have been updated... Waiting for deployment "api" rollout to finish: 4 out of 5 new replicas have been updated... Waiting for deployment "api" rollout to finish: 1 old replicas are pending termination... deployment "api" successfully rolled out deployment.apps/api rolled back
In practice you would edit the tag in the YAML and commit it, then let CI or a GitOps tool such as Argo CD or Flux apply it, so git remains the record of what is running. Helm and Kustomize help manage the YAML once you have several environments.
Recap
- Kubernetes runs the same images across many machines, restarting, scaling and rolling them out for you.
- You declare desired state in YAML with
kubectl apply; controllers make reality match. - Deployment is roughly a Compose service; a Service gives stable DNS and load balancing via label selectors.
- ConfigMaps and Secrets hold configuration; Secrets are only base64-encoded by default.
kubectl set image,rollout statusandrollout undogive zero-downtime updates and quick rollbacks.
# Write your solution here
Finished reading? Mark this lesson complete to track your progress.
