University of Wales Trinity Saint David
ACCA7023 - Agile and DevOps

Docker at Scale

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:

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.

scale/compose.yaml
# 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.

scale/nginx.conf
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 up again, 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?

Reconciliation closes the gap between desired and actual state automatically.

During a rolling update from v1 to v2, what is true?

Old and new versions overlap, which is why database changes are made in backward compatible steps.

Why do orchestrators need the health checks you added in the Building Efficient Images lab?

A running process is not necessarily a working one. Health checks give the orchestrator a reliable signal.

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.

quotes-api.yaml
# 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 termMeaningClosest thing you have used
Cluster and NodeA group of machines managed as one, and each machine in itYour one machine running Docker
PodThe smallest unit Kubernetes runs, one or more containers that are always placed togetherA container
DeploymentDesired state for a set of identical pods, including how to update themA Compose service with --scale
ServiceA stable name and address that load balances across the matching podsThe service name on a Compose network plus your nginx load balancer
SecretSensitive configuration stored separately from the YAMLYour .env file
ProbesReadiness 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?

At most one replica may be unavailable at a time, so with three desired, at least two keep serving.

Which field in the file decides which pods the Service sends traffic to?

Kubernetes connects objects with labels and selectors rather than by name.

Task 7 - Choose a platform

Kubernetes is powerful and also complex to run well. Most organisations choose from a spectrum.

OptionExamplesSuits
Compose on one serverDocker ComposeDevelopment, demos, small internal tools where some downtime is acceptable
Serverless containersAzure Container Apps, Google Cloud Run, AWS FargateWeb services and APIs where you want scaling and rolling updates without managing a cluster
Managed KubernetesAzure Kubernetes Service, Amazon EKS, Google GKEMany 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.

The API is stateless. It keeps its data in Postgres, takes its configuration from environment variables, logs to standard output and can be started and stopped at any time. Any replica can therefore answer any request. With in-memory storage each replica would hold its own separate list, so a quote added through one replica would be missing when the load balancer sent the next request to another, and every crash or restart would lose data. Scaling would make the product appear randomly inconsistent.

A start-up with two developers and one web API asks whether it should adopt Kubernetes. Give a reasoned recommendation.

Probably not yet. Kubernetes brings a large operational burden in upgrades, networking, security and monitoring that a two-person team would spend time on instead of the product. A serverless container platform such as Azure Container Apps or Google Cloud Run gives replicas, autoscaling, health checks and rolling updates for the same container image with almost no cluster management. Because the application is already containerised, moving to Kubernetes later, if the number of services or the need for control grows, would not require rewriting it. A good answer weighs team size, number of services, cost and the need for control.

Explain the difference between scaling up and scaling out, and why containers suit scaling out.

Scaling up, or vertical scaling, means giving one instance more CPU and memory. It is limited by the largest machine available and usually needs a restart. Scaling out, or horizontal scaling, means adding more identical instances behind a load balancer. It has no fixed ceiling and also improves availability because no single instance is critical. Containers suit scaling out because they start in seconds, are identical by construction because they come from the same image, and can be placed on any machine with a container runtime.