Overview
Everything so far has run one copy of each container on one computer. Real services run many copies across many computers, survive crashes without anyone noticing, and release new versions without going offline. This lab first shows the core ideas on your own machine, by running several copies of the API behind a load balancer and crashing one. It then explains how orchestrators such as Kubernetes apply the same ideas across a whole data centre, and why the images you have been building are ready for that.
Learning Objectives
By the end of this lab you will be able to:
- Run several replicas of a service behind a load balancer with Docker Compose
- Demonstrate load balancing and recovery from a crashed replica
- Explain why stateless services can be scaled freely and stateful ones cannot
- Describe desired state, reconciliation, self-healing and rolling updates
- Identify the main objects in a Kubernetes Deployment and Service
- Compare Compose, Kubernetes and managed container platforms for a given situation
Concept
A single container on a single machine has three weaknesses. If it crashes, the service is down. If traffic grows beyond what one process can handle, the service slows down. And replacing it with a new version means stopping the old one first, so there is a gap. Running several identical copies, called replicas, behind a load balancer addresses all three. The load balancer shares requests between replicas and stops sending traffic to any that fail.
This only works if replicas really are identical and interchangeable, which means they must be stateless. A replica must not keep anything in memory or on its own disk that another replica would need, such as user sessions or uploaded files. State belongs in a database, a cache or object storage that every replica shares. This is the main reason the quotes API stores its data in Postgres and why you configured it through environment variables. Both are principles of the widely cited Twelve-Factor App guidelines for cloud software.
Managing replicas by hand across many machines quickly becomes impossible. An orchestrator does it for you. You give it a desired state, such as three replicas of quotes-api version 1.0, and it continuously compares that with the actual state and acts to close any gap. This loop is called reconciliation. If a replica crashes or a whole machine fails, it starts replacements, which is self-healing. If you change the desired version, it replaces replicas a few at a time, which is a rolling update. Kubernetes, originally developed at Google, is by far the most widely used orchestrator, and every major cloud offers it as a managed service.
Tasks
Task 1 - Describe several replicas
Inside your quotes folder create a folder called scale with two files. The compose file runs the API and an nginx load balancer in front of it. Notice what the api service does not have. It has no ports, because several replicas cannot all publish the same port on your machine, and no fixed container name, because several containers cannot share one.
# Several copies of the API behind one load balancer.
# Start it with: docker compose -f scale/compose.yaml up -d --build --scale api=3
services:
lb:
image: nginx:1.29-alpine
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "8090:80"
depends_on:
- api
api:
build: ../quotes-api
environment:
APP_VERSION: scaled
restart: always
The load balancer configuration asks Docker's DNS server for api every few seconds. When a service has several replicas, Docker's DNS answers with the address of every one of them, and nginx shares requests between them.
server {
listen 80;
# Ask Docker's DNS for "api" again every few seconds. It answers with the
# address of every replica, and nginx shares requests between them.
resolver 127.0.0.11 valid=5s;
set $api http://api:3000;
location / {
proxy_pass $api;
}
}
restart: always tells Docker to restart a replica whenever its process exits, for any reason.
Task 2 - Start three replicas
Run these from the quotes folder. -f chooses the compose file. --scale sets how many containers to run for a service.
docker compose -f scale/compose.yaml up -d --build --scale api=3
docker compose -f scale/compose.yaml ps
Success condition: four containers, named scale-lb-1 and scale-api-1 to scale-api-3. Now send six requests to the endpoint that reports which container answered.
for i in 1 2 3 4 5 6; do curl -s http://localhost:8090/api/whoami; echo; done
Success condition: the answers come from three different host names. Your machine sees one address, and behind it requests are being shared out.
Task 3 - Crash a replica
Simulate a crash by stopping the main process inside the second replica, and immediately send a stream of requests. %{http_code} makes curl print only the response status.
docker compose -f scale/compose.yaml exec --index 2 api kill 1
for i in $(seq 1 12); do curl -s -o /dev/null -w "%{http_code} " http://localhost:8090/api/health; sleep 0.3; done
docker compose -f scale/compose.yaml ps
Success condition: every request returns 200, and scale-api-2 has a much shorter Up time than the others because Docker restarted it. A user would never have known. Now change the number of replicas while the service is running.
docker compose -f scale/compose.yaml up -d --scale api=5
docker compose -f scale/compose.yaml ps
docker compose -f scale/compose.yaml up -d --scale api=1
Task 4 - Find the limits of Compose
What you just did shows the right ideas, but Compose has hard limits as a production platform. Consider each question before reading on.
- All five replicas ran on your laptop. What happens to the service if the laptop is switched off?
- You set the number of replicas by hand. Who changes it at three in the morning when traffic triples?
- To release a new version you would run
upagain, which recreates every replica at once. Is there a gap? - Try scaling the database the same way. Five Postgres containers would each have their own separate data. Which one is right?
Compose manages containers on one host. Surviving the loss of a machine, scaling automatically, and updating without downtime need a system that manages a cluster of machines as one. That is what orchestrators do.
docker compose -f scale/compose.yaml down
Task 5 - How an orchestrator thinks
This simulator behaves like an orchestrator. Scale up and down, crash replicas and roll out a new version. Watch the desired and actual lines and read each explanation.
An orchestrator is told to run 4 replicas. A machine fails and takes 2 of them with it. What does the orchestrator do?
During a rolling update from v1 to v2, what is true?
Why do orchestrators need the health checks you added in the Building Efficient Images lab?
Task 6 - Read a Kubernetes Deployment
You will not run Kubernetes in this series, but you should be able to read its configuration. Kubernetes is configured with YAML files describing desired state. Read this one, which asks for the quotes API to run in a cluster.
# For reading only. This series does not run Kubernetes.
# A Deployment keeps a set number of identical pods running.
apiVersion: apps/v1
kind: Deployment
metadata:
name: quotes-api
spec:
replicas: 3
selector:
matchLabels:
app: quotes-api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: quotes-api
spec:
containers:
- name: api
image: docker.io/jsmith/quotes-api:1.0
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: quotes-db
key: url
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /api/health
port: 3000
livenessProbe:
httpGet:
path: /api/health
port: 3000
---
# A Service gives the pods one stable name and shares traffic between them.
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: quotes-api
ports:
- port: 3000
targetPort: 3000
| Kubernetes term | Meaning | Closest thing you have used |
|---|---|---|
| Cluster and Node | A group of machines managed as one, and each machine in it | Your one machine running Docker |
| Pod | The smallest unit Kubernetes runs, one or more containers that are always placed together | A container |
| Deployment | Desired state for a set of identical pods, including how to update them | A Compose service with --scale |
| Service | A stable name and address that load balances across the matching pods | The service name on a Compose network plus your nginx load balancer |
| Secret | Sensitive configuration stored separately from the YAML | Your .env file |
| Probes | Readiness decides whether a pod gets traffic. Liveness decides whether it is restarted. | HEALTHCHECK |
In the Deployment, what does maxUnavailable: 1 under rollingUpdate guarantee during an update of 3 replicas?
Which field in the file decides which pods the Service sends traffic to?
Task 7 - Choose a platform
Kubernetes is powerful and also complex to run well. Most organisations choose from a spectrum.
| Option | Examples | Suits |
|---|---|---|
| Compose on one server | Docker Compose | Development, demos, small internal tools where some downtime is acceptable |
| Serverless containers | Azure Container Apps, Google Cloud Run, AWS Fargate | Web services and APIs where you want scaling and rolling updates without managing a cluster |
| Managed Kubernetes | Azure Kubernetes Service, Amazon EKS, Google GKE | Many services, platform teams, needs for fine control over networking, scheduling and security |
All three run the same images. That portability is what makes the effort you have put into small, stateless, configurable images pay off. The Continuous Deployment to Azure Container Apps lab later in the module deploys to the middle option.
Task 8 - Evaluate
The quotes API scaled to five replicas with no changes to its code. Explain which design decisions made that possible, and what would have gone wrong if the API had kept quotes in memory.
A start-up with two developers and one web API asks whether it should adopt Kubernetes. Give a reasoned recommendation.
Explain the difference between scaling up and scaling out, and why containers suit scaling out.