
EE 547 - Unit 2
Fall 2026
Running Distributed Components
One application to its users. Five programs to the machine:
Any system of size decomposes the same way
Separate processes
Separate management

A process gets everything it uses from the machine, through the operating system.
CPU - instructions executed
Memory - working data held resident
Disk - data that must survive the process
Network
The mix is process-specific
A machine is specified for the peak demand of what runs on it.

One process per physical server, each sized for its own peak.
Peak sizing
Waiting
Failover headroom
Powered and paid for around the clock; 80-90% of it doing nothing.

Total cost of ownership is fixed by the hardware, not by the work it does:
| Component | Cost |
|---|---|
| Hardware, $8,000-15,000 over 3-4 years | ~$250/month |
| Power (400 W, continuous) | ~$35/month |
| Cooling (40% of power) | ~$14/month |
| Rack space, networking | ~$50/month |
| Total | ~$350/month |
Cost per unit of useful work - TCO divided by utilization. At 15%:
\[\frac{\$350}{0.15} \approx \$2{,}300 \text{ per month per fully used server}\]
Five processes, five servers - $1,750 per month for the work of 0.75 servers.

At datacenter scale - thousands of servers, millions of dollars a year on idle capacity. Recovering it is what virtualization was built for.
Server consolidation: several processes on one physical machine instead of one machine each.

Utilization
What is shared - The hardware, and one operating system with it:
Each shared element is a path by which one process affects another.
On one OS installation:
Python 3.7 and 3.11 - no conflict; interpreters install side by side
requests 2.22 and 2.31 - no conflict; each application has its own virtual environment
libssl 1.1 and libssl 3 - conflict
Virtual environments isolate Python packages

Starvation - a process cannot get a resource because another process holds it
nice, ulimit); nothing bounds an application as a wholeMemory
CPU
Disk I/O
The fault is in A, the symptom in B, and the OS records no connection between them.

Permissions are enforced per user account, not per application
Files - A reads B’s configuration, credentials included
Processes - every command line on the machine is visible to every process
Ports - when B restarts, A can bind B’s port first and receive B’s traffic
An exploit against either application yields both.

Every process on the machine runs on the same kernel. A kernel event is an event for all of them.
Patching
Crashes
Attribution
/var/log, one CPU graph, one diskIndependently developed and operated, the applications now fail together.

| Dedicated servers | Shared OS | |
|---|---|---|
| Utilization | 10-20% | ~60% |
| Dependencies | Independent per application | One set for all |
| Resource limits | Physical | None between applications |
| Security boundary | Physical | Shared accounts, ports, files |
| Failure domain | One application | Every application |
Required, per application
All of it at shared-server utilization.
Virtualization and containers both provide these; they differ in how strong the boundary is and what it costs.
Every interface has an address; every packet carries two
IPv4
192.168.1.10/24
└───┬───┘ └┬┘
network host 256 addresses: .0 to .255
IPv6
2001:db8:85a3::8a2e:370:7334Loopback
127.0.0.1, localhost: this machine
Public IPv4 addresses are exhausted - Three ranges are reserved for use inside a network and never appear on the public internet:
| Range | Addresses | Typical use |
|---|---|---|
10.0.0.0/8 |
16.8M | Corporate networks, cloud VPCs |
172.16.0.0/12 |
1M | Docker’s default network (172.17.0.0/16) |
192.168.0.0/16 |
65K | Home and small-office networks |
Not routable on the internet
192.168.1.10 exists in millions of networks at onceNAT
Consequence

A web server starts and binds to port 80:
What the OS records - port 80 → this process
Port numbers - 16 bits, 0-65535

One IP address, many listening processes. The destination port selects which one:
Destination: 192.168.1.10:5432
└─────┬─────┘ └┬─┘
machine process
| Port | Service | Port | Service | |
|---|---|---|---|---|
| 22 | SSH | 5432 | PostgreSQL | |
| 80 | HTTP | 6379 | Redis | |
| 443 | HTTPS | 8080 | Alternate HTTP |
One process per port
bind() to port 80 fails: OSError: [Errno 98] Address already in use
Destination - The server’s address and port, named by the client.
Source - The client’s address, plus a port the OS picks from the ephemeral range (Linux default 32768-60999).
A connection is a 4-tuple - source address, source port, destination address, destination port

Stream to segments
What a segment carries
IP header src 10.0.0.5 dst 192.168.1.10
TCP header src port 52431 dst port 5432
seq 1460 ack 88 flags ACK window 65535
data bytes 1460-2919 of the stream
Sequence numbers count bytes
A connection is state at both ends

IP alone - Packets delivered one at a time; some lost, some out of order, none acknowledged.
TCP on top - A byte stream with four guarantees:
Reliable
Ordered
Flow-controlled - the receiver advertises its free buffer space; the sender stays within it
Connection-oriented - a handshake creates state at both ends before data flows; a close releases it
Application code calls send() and recv(); retransmission, reordering, and pacing happen in the kernel.

Connection setup
Head-of-line blocking
Header overhead
Where it is worth paying

UDP delivers datagrams the way IP delivers packets: individually, unacknowledged.
No handshake - the first packet carries data; zero setup round trips
No retransmission, no ordering, no flow control
8-byte header - versus 20 for TCP
Where it fits - wherever loss costs less than delay

| Ethernet 14 B | IP 20 B | TCP 20 B | HTTP request ... |
link network transport application
Application - HTTP, the PostgreSQL protocol, DNS: what the bytes mean
Transport - TCP, UDP: which process (port), and whether delivery is reliable
Network - IP: which machine (address), and the route across networks to reach it
Link - Ethernet, Wi-Fi: the next hop on the local segment (MAC address)

One line of application code. Underneath it:
Application - build the HTTP request text; parse the response
Name resolution - look up api.example.com with DNS (an application protocol itself, carried over UDP)
Transport - open a TCP connection to port 80: handshake, send, retransmit as needed, close
Network - route each packet toward the destination address across many routers
Link - put each frame on the wire, or the air, to the next hop
Cost of the abstraction

Resolution
getaddrinfo("api.example.com").com → example.comTTL
Why names
db, api.example.com), not to an address
A virtual machine is a computer whose hardware is provided by software.
What the guest sees
What exists
What keeps them apart

One host, several complete computers
Isolation
Efficiency
Isolation and efficiency on one machine - the hypervisor provides both; the guest operating systems are what they cost

The hypervisor is the software layer between physical hardware and the guest operating systems.
Presents virtual hardware - Each VM gets virtual CPUs, memory, disks, and network cards backed by slices of the real ones.
Schedules and allocates - Decides which VM’s vCPU runs on which core, and which physical pages hold which VM’s memory.
Intercepts privileged operations
Result

Type 1: bare metal

Type 2: hosted

Scheduling
Hardware assistance
Overcommit
Steal time

The guest’s view
Two translations
Isolation
0x1000 and VM 2’s guest-physical 0x1000 land on different host pagesCost

Virtual disk
/dev/xvda is a file on the host (qcow2, VMDK, or a raw block device)Virtual network card - The guest’s eth0 connects to a virtual switch in the hypervisor, and from there:

The guest kernel, its init system, and its system files exist once per VM.
| Per VM | Overhead |
|---|---|
| Memory | 0.5-2 GB for the guest OS |
| Disk | 5-20 GB for the OS installation |
| Boot | 30 s to 2 min before the application starts |
| CPU | Hypervisor scheduling and trapping |
Ten VMs on a 256 GB host - 5-20 GB of RAM and 50-200 GB of disk hold ten copies of the same operating system, doing no application work.
Density limit - Every VM added costs another full operating system before it runs any application code.

Security
Hardware-enforced - CPU privilege levels and memory mappings are checked by the processor on every instruction, not by software.
Heterogeneous - Windows beside Linux; a 2014 kernel beside a current one; whatever the application was certified on.
Where the boundary is required
Where it is more than needed
The instance type is the VM’s shape
t3.medium: 2 vCPUs, 4 GB memory, network up to 5 Gbit/sBurstable
t3 vCPUs earn CPU credits while idle and spend them under loadThe hypervisor
What is hidden

One program holds full hardware privilege: the kernel. Everything else asks it.
Hardware access
Process management
Resource allocation
The boundary between software and hardware

A process cannot do any of these on its own:
Each is a system call: a request to the kernel, which checks it, performs it, and returns the result.
The kernel answers every question a process asks
A process knows only what the kernel tells it.

On an ordinary Linux system the kernel maintains a single, global view.
One process table
ps shows all of them, to anyoneOne filesystem tree
/One network stack
One set of users
Two applications on the machine share all four
/tmpThat shared view is the source of every problem in the first section.

A container is not a virtual machine. It runs no kernel of its own.
A container is a process (or a group of processes) that the kernel treats specially
/?” and gets a filtered subsetNamespaces are the kernel feature that does the filtering
Same kernel, different views

Each container has its own process-ID space.
Inside the container
On the host
inside container A on the host
PID COMMAND PID COMMAND
1 /app/server 4523 /app/server
2 /app/worker 4524 /app/worker
4611 /app/server (container B)
Consequence

Each container has its own view of the filesystem, rooted in its own directory tree.
container A: / container B: / host: /
├── bin ├── bin ├── bin
├── etc ├── etc ├── boot
├── lib ├── lib64 ├── dev
├── usr ├── opt ├── etc
└── app └── data ├── home
└── var/lib/containers/
├── A/ ← container A's /
└── B/ ← container B's /
Separate trees
/etc/passwd and B’s /etc/passwd are different filesDifferent contents
libssl versions, different everything above the kernelExplicit exceptions

Each container has its own network stack:
eth0 inside the container is not the host’s eth0172.17.0.0/16Port conflicts disappear
Reaching the outside
The remaining constraint

Linux provides one namespace type per kind of resource. A container uses all of them at once.
| Namespace | Isolates | The container sees |
|---|---|---|
| PID | Process IDs | Its own process tree, starting at 1 |
| Mount | Filesystem | Its own root and mounts |
| Network | Network stack | Its own interfaces, addresses, ports |
| UTS | Hostname | Its own hostname |
| IPC | Shared memory, semaphores | Only its own |
| User | User and group IDs | UID 0 inside may be an unprivileged UID outside |
Each namespace hides one thing
Mechanism
Namespaces control what a process can see. They do not control what it can use.
Without limits, one container could still take
Control groups (cgroups) let the kernel account for and limit resources per group of processes
Enforcement
The gap from the first section, closed

Docker exposes cgroups as flags on docker run.
CPU
Memory
Why limits matter
What limits cost
What limits do not do
--memory=2g on a 3 GB host can both be within their limits and still exhaust it

Virtual machine
Container
Same two goals from the first section. Different layer, different cost, different strength.
A VM boots
A container starts a process
What that changes

The same 256 GB host, two ways.
Running VMs
| Memory | |
|---|---|
| Hypervisor | ~2 GB |
| Guest OS, per VM | ~1-2 GB |
| Available for 10 VMs’ applications | ~236 GB |
Running containers
| Memory | |
|---|---|
| Host OS | ~2 GB |
| Container runtime | ~200 MB |
| Per-container overhead | ~10-50 MB |
| Available for applications | ~253 GB |
Result

Compatibility
Security
Hardening narrows the surface
seccomp filters which system calls a container may makeWhere containers are the right boundary
Where a VM is still required
Layered in practice
The Linux kernel has had namespaces since 2002 and cgroups since 2007. A container was possible long before Docker.
Assembling one by hand
Why almost nobody did

Docker, released in 2013, made containers usable, not possible. The kernel mechanisms are the same; one tool drives them.
Image format
Build system
Distribution
Runtime

Image - a read-only template
Container - a running instance of an image
One image, many containers

An image is not a single blob. It is a stack of layers, each one a change on the one below:
apt install pythonpip install flaskLayers are read-only and content-addressed
Image A Image B
┌───────────┐ ┌───────────┐
│ my app │ │ other app │
├───────────┤ ├───────────┤
│ flask │ │ django │
├───────────┼───────┼───────────┤
│ python:3.11 │
├─────────────────────────────┤
│ ubuntu │
└─────────────────────────────┘
(shared layers)

Storage
python:3.11-slim share one copy of that layerDistribution
Build
COPY . /app layer is rebuiltLayer ordering matters

Multiple read-only layers must appear to the container as one filesystem. OverlayFS stacks them:
The writable layer
Copy-on-write
Why containers are ephemeral
Read-only layers (from image):
┌─────────────────────────┐
│ Layer 3: COPY . /app │
├─────────────────────────┤
│ Layer 2: pip install ...│
├─────────────────────────┤
│ Layer 1: python:3.11 │
└─────────────────────────┘
Writable layer (per container):
┌─────────────────────────┐
│ /app/data/cache.db [new]│
│ /tmp/session.txt [new]│
└─────────────────────────┘
Container sees merged view:
/app/ ← Layer 3
/usr/bin/python ← Layer 2
/bin/bash ← Layer 1
/app/data/cache.db ← writable
A Dockerfile is a text file of instructions for building an image. Each instruction creates a layer.
# Base image
FROM python:3.11-slim
# Working directory inside the image
WORKDIR /app
# Dependencies first: cached while requirements.txt is unchanged
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Application code last: changes most often
COPY . .
# Document the port the application listens on
EXPOSE 8080
# Command to run when the container starts
CMD ["python", "app.py"]Order matters for cache efficiency: dependencies before application code.
Instructions
| Instruction | Purpose |
|---|---|
FROM |
Base image to start from |
WORKDIR |
Set the working directory |
COPY |
Add files from the build context |
RUN |
Execute a command during the build |
ENV |
Set an environment variable |
EXPOSE |
Document a listening port |
CMD |
Default command to run |
ENTRYPOINT |
Command that always runs |
The build process
myapp:v1The build context
. names the build context: the directory whose contents COPY can see.dockerignore excludes what is not needed
What Docker does
CMD or ENTRYPOINTForeground or detached
Naming
--name, Docker assigns a random name
A container’s network namespace has its own ports. A container listening on port 80 is not reachable from outside the host by default.
Port mapping connects a host port to a container port:
Host Container
┌──────────┐ -p 8080:80 ┌──────────┐
│ :8080 │ ──────────────> │ :80 │
└──────────┘ └──────────┘
Multiple mappings
Two containers, both on port 80 - map them to different host ports: -p 8080:80 and -p 8081:80

Changes to a container’s filesystem live in its writable layer. Remove the container and that layer is deleted.
$ docker run -d --name mydb postgres
$ docker exec mydb psql -c "CREATE TABLE users ..."
# database files written to the container's writable layer
$ docker rm -f mydb
# container removed: the writable layer, and the data, are gone
$ docker run -d --name mydb postgres
# a fresh container: no users tableThis is by design
What must outlive the container

A volume is storage managed by Docker that exists outside any container’s filesystem.
Named volume
pgdata persists when the container is removedBind mount - a host directory mounted into the container

Many applications read their configuration from environment variables. Docker sets them at run time:
Inside the container:
Why environment variables
Limit - docker inspect shows a container’s environment to anyone who can run it; a secrets manager is the production answer

Containers on the same Docker network reach each other by container name.
How api reaches db
db resolves to the database container’s address on that networkNo port mapping needed

Images move between machines: build server to production, one workstation to another. A registry is the server they move through.
Docker Hub - the default public registry
Private registries - for proprietary images: Amazon ECR, Google Artifact Registry, Azure Container Registry, or self-hosted
Image naming
registry/repository:tag
gcr.io/my-project/myapp:v1.2.3
latest
# Create and start
$ docker run -d --name web nginx
# state: running
# Stop (graceful shutdown: SIGTERM, then SIGKILL after 10 s)
$ docker stop web
# state: exited
# Start again, same container, same writable layer
$ docker start web
# state: running
# Remove
$ docker rm web
# container and its writable layer deleted
# Force-remove a running container
$ docker rm -f webListing

| Command | Purpose |
|---|---|
docker build -t name:tag . |
Build an image from a Dockerfile |
docker run image |
Create and start a container |
docker run -d |
Run detached, in the background |
docker run -p host:container |
Map a host port to a container port |
docker run -v vol:/path |
Mount a volume |
docker run -e VAR=value |
Set an environment variable |
docker ps |
List running containers (-a: all) |
docker logs container |
Show a container’s output |
docker exec -it container sh |
Open a shell in a running container |
docker stop container |
Stop a container |
docker rm container |
Remove a container |
docker images |
List images |
docker pull image |
Download an image from a registry |
docker push image |
Upload an image to a registry |
The “works on my machine” problem
PATH includes local toolsWhat the image guarantees
What that removes

The image settles what is inside one container. Everything between containers is still open.
Several containers
Several hosts
docker run acts on one machineConfiguration and secrets
The kernel
Several containers on one host: Compose. Several hosts: orchestration.

docker network create myapp
docker run -d --name db --network myapp \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:15
docker run -d --name cache --network myapp redis:7
docker run -d --name api --network myapp \
-e DATABASE_URL=postgres://db:5432 \
-e REDIS_URL=redis://cache:6379 \
myapp-api:latest
docker run -d --name worker --network myapp \
-e DATABASE_URL=postgres://db:5432 \
-e REDIS_URL=redis://cache:6379 \
myapp-worker:latest
docker run -d --name web --network myapp \
-p 80:80 nginx:latestEach docker run acts once, with its own flags, at its place in the sequence. Nothing ties the commands together except the order they were typed.
Every start
Every other machine
Every update
Nothing records what the running application is supposed to look like. The commands are the only description, and they live in a shell history.
services:
db:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
cache:
image: redis:7
api:
build: ./api
environment:
DATABASE_URL: postgres://db:5432
REDIS_URL: redis://cache:6379
depends_on: [db, cache]
worker:
build: ./worker
environment:
DATABASE_URL: postgres://db:5432
REDIS_URL: redis://cache:6379
depends_on: [db, cache]
web:
image: nginx:latest
ports:
- "80:80"
depends_on: [api]
volumes:
pgdata:One command. The file is the description the commands never had: what should be running, with what, connected how.
Each docker run flag has a place in the file
| Command line | Compose file |
|---|---|
docker run --name db |
services: db: |
postgres:15 |
image: postgres:15 |
-v pgdata:/var/... |
volumes: under the service |
-e POSTGRES_PASSWORD=... |
environment: |
-p 80:80 |
ports: |
--network myapp |
implicit: one network for the file |
| start order, by hand | depends_on: |
docker build in ./api |
build: ./api |
What changes
docker compose up againYAML is a superset of JSON: any JSON document is valid YAML. Compose files, Kubernetes manifests, and most cloud configuration use it because nested structure reads cleanly.
Syntax
key: value pairs-#Where it goes wrong - unquoted scalars are typed by guess
| Written | Read as | Write instead |
|---|---|---|
version: 1.10 |
the float 1.1 |
version: "1.10" |
DEBUG: yes |
the boolean true |
DEBUG: "yes" |
enabled: on |
the boolean true |
enabled: "on" |
message: Error: failed |
parse error (second colon) | message: "Error: failed" |
| a tab for indentation | parse failure, often silent | spaces |
port: 08 |
may be read as octal | port: "08" |
Rule - anything that must stay a string, quote it. Environment-variable values in particular: they are always strings to the application.
services - one entry per container (or per set of identical containers when scaled)
volumes - named volumes that outlive containers, referenced from services
networks - only when the default is not enough
Options under a service
| Key | Meaning | docker run equivalent |
|---|---|---|
image: |
Use an existing image | positional image |
build: |
Build from a directory with a Dockerfile | docker build |
ports: |
"host:container" mappings |
-p |
environment: |
Variables set in the container | -e |
env_file: |
Variables read from a file | --env-file |
volumes: |
Named volume or bind mount, source:target |
-v |
depends_on: |
Services that must be started first | manual ordering |
restart: |
no, always, on-failure, unless-stopped |
--restart |
healthcheck: |
Command Compose runs to judge readiness | --health-cmd |
The default network
<project>_default
What Compose does
db container firstapi as soon as db has startedWhat it does not wait for
The race
api starts, connects, fails: connection refusedOption 1: the application retries
Option 2: Compose waits for a health check
api is not started until pg_isready succeedsBoth, in practice: the health check for a clean start, the retry for everything after it.
When docker compose up runs, Compose:
<project>_defaultWhat that gives the application
api connects to db:5432; DNS resolves db to the database container’s current addressdocker network create, no --network flags
Bring the application up and down
Inspect
Run something inside
Scope
myapp-api-1, myapp_default, myapp_pgdata--scale
Mount the source for live reload
Override files for environments
docker-compose.yaml base: services, images, volumes
docker-compose.override.yaml dev: bind mounts, debug flags (loaded automatically)
docker-compose.prod.yaml prod: restart policies, no mounts

What it does well
docker compose up gives anyone the same stackWhat it does not do
--scale is a number a person typesdocker compose up after a change restarts containers, with a gapEvery item on the right is a consequence of one fact: Compose talks to one Docker engine. Spanning machines is orchestration.

Compose runs the whole application on one machine. Three things end that arrangement.
It fills up
It fails
It needs a reboot
More than one host removes all three. It also creates work that did not exist before.

With containers on several machines, five tasks recur every time a container starts, moves, or dies.
Place - choose a machine with room for the container’s CPU and memory
Connect - a container on host A must reach one on host B; each host has its own bridge and its own 172.17.0.0/16
Find - addresses change whenever a container restarts or moves; the others must still locate it
Replace - a container crashes, or a whole host does; something must notice and start it again elsewhere
Update - a new version must go in without a gap; old containers leave as new ones prove healthy
By hand, each is a command someone runs, on the right host, at the right moment, indefinitely.
An orchestrator is software that does the five tasks, from a description of the application, on every machine in a cluster, without stopping.
Input - a description, as a compose file is for one host
Loop
Systems


A Kubernetes cluster has two kinds of machines.
Control plane - holds the description and issues instructions
| Component | Does |
|---|---|
| API server | Receives every request, from kubectl or from inside the cluster |
| Scheduler | Picks a node for each new pod |
| Controller manager | Compares what should run with what is running; corrects the difference |
| etcd | Stores the cluster’s description and current state |
Worker nodes - run the containers and report back
| Component | Does |
|---|---|
| kubelet | Starts, monitors, and reports its node’s containers |
| Container runtime | Runs them: containerd, CRI-O |
| kube-proxy | Routes service traffic arriving at the node |
Application containers run only on worker nodes.
Kubernetes does not place containers one at a time. It places pods: one or more containers that always land on the same machine together.
Containers in one pod share
localhost between themWhy a group exists
Almost always, one container
More capacity means more pods of one container, never more containers in a pod.


A pod has no fixed home and no fixed address. At any time it may be
The replacement is a new pod with a new IP address.
Two rules follow
Two objects make the rules workable
Pods are rarely created one at a time. A Deployment states how many copies of a pod should run; Kubernetes keeps that many running.
The file states
replicas
Kubernetes keeps it true
replicas: 3: exactly three, whatever happens to any one of themThe file states the count. Kubernetes performs every action needed to hold it there.

Changing the Deployment’s image starts a rolling update:
No gap - some pods serve at every moment
Automatic stop - if new pods fail their health checks, the rollout halts with the old pods still serving
Pods come and go with new addresses. A Service is the fixed point in front of them.
A Service has
web-server.default.svc.cluster.localWhen a pod is replaced, traffic reaches the replacement. Clients only ever know the Service.

Who can reach it
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP |
Inside the cluster only | Databases, caches, internal APIs |
NodePort |
Any node’s IP, on a fixed high port | Development, quick tests |
LoadBalancer |
A public IP through the cloud’s load balancer | Production entry point |
HTTP routing across many services usually goes through an Ingress in front of ClusterIP services.
ConfigMap - settings that are not sensitive
Secret - sensitive values
Injected into pods as environment variables or as files
Why separate objects
The same rule as docker run -e, with access control on who may read the values.
| Scenario | Tool |
|---|---|
| One container, a quick test | docker run |
| Several containers, local development | Docker Compose |
| Several containers, CI testing | Docker Compose |
| Small production deployment on one host | Docker Compose, with monitoring |
| Production that must survive a host failure, or outgrow one host | Kubernetes, managed |
| Many services, many hosts | Kubernetes |
Compose, until a requirement forces more
Managed control planes
The concepts transfer
Where these components run