CAMPUX Cloud Bootcamp Phase Four · Class Twenty-Six
Phase Four — Operate, Secure & AI
Reading 40 min · Drills 6 · 2 Labs
Build IV foundations
Class Twenty-Six

Containers & Docker

"It works on my machine" has cost the software industry more money than any outage — containers are the invoice-ending answer: ship the machine along with the work.

§1

The problem, priced

Here is a bill you have not seen itemised before. An application works on the developer's laptop — Python 3.12, a system library from last spring, an environment variable set two jobs ago and long forgotten. It reaches the server, which runs Python 3.9 and a different library, and fails at two in the morning. Now price it: hours of two engineers diffing environments; a release slipped by a week; a "deployment runbook" that grows another fragile page; and, at the multiplied scale of a real company, whole teams whose actual job is reconciling machines. The phrase "works on my machine" is a joke because the alternative is crying about the invoice.

Every fix before containers shared one flaw: it shipped the application and hoped about the environment. Virtual machines fixed the hoping by shipping the entire computer — operating system and all — at gigabytes per copy and minutes per boot, which is how Class Eleven's compute bill got its size. The container's insight is surgical: applications do not disagree about kernels, they disagree about everything stacked above the kernel — runtimes, libraries, configuration. So package exactly that layer, share the host's kernel, and the artifact drops from gigabytes to megabytes and from minutes to milliseconds while still behaving identically everywhere it lands.1

Container
A running process wrapped in its own filesystem — application, runtime, libraries, configuration — isolated from the host and sharing only its kernel. The environment stops being a property of the server and becomes a property of the artifact.
§2

Images, containers, registries

Three nouns carry the whole vocabulary, and the relationships between them matter more than the definitions. Docker is the tool that made all three mainstream; the concepts now outrun any one vendor.2

Image
The frozen artifact: a read-only, layered snapshot of the filesystem your application needs. Built once, hashed, versioned with tags like campux/pos-batch:1.4. It does nothing by itself — it is the recipe box, not the meal.
Container
An image, running. docker run takes the frozen image, adds a thin writable layer on top, and starts the process. Ten containers can run from one image; when a container is deleted its writable layer — everything it changed — is gone. Containers are cattle by construction.
Registry
Where images live between machines: the Git-remote of the container world. Docker Hub is the public one; Azure Container Registry is yours. push after building, pull before running — the same verbs as Class Seventeen, moving filesystems instead of commits.

The analogy that makes it click is one you already own: an image is to a container what a class is to an object — or a Bicep file to a deployment. One definition, many instances, and the instances are disposable while the definition is precious. That disposability is a discipline, not a limitation: anything a container writes that must survive — the database file, the uploaded receipts — belongs in a volume or an Azure storage account mounted in from outside, never in the container itself. Engineers who treat containers as small pet VMs, patching and grooming a running instance, rebuild Class Nineteen's drift problem one process at a time. The image is the truth; the container is one Tuesday's copy of it.

§3

Dockerfile literacy — layers, caching, small images

Images are built from a Dockerfile — a short script of instructions, each of which lays down one read-only layer on top of the last. Here is the Campux batch job's, and it is deliberately ordinary:

# Dockerfile — the POS batch job
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "batch.py"]

The layering is not trivia; it is the economics of your build. Docker caches every layer, and a layer is rebuilt only if its instruction — or any layer beneath it — changed. Read the order above with that rule: the base image and the dependency install sit at the bottom, changing rarely; your code, which changes twenty times a day, is copied last. Edit batch.py and only the final layers rebuild — seconds. Now imagine COPY . . moved above the pip install: every code edit would invalidate the copy layer and everything above it, and the full dependency download would rerun on every build, all day, forever. One transposed line, a hundred slow builds a week.

Figure 14 — Image layers drawn as sediment strata, with the frequently-changing layer annotated as the one that busts the cache FROM python:3.12-slim bedrock — changes rarely COPY requirements.txt RUN pip install slow to lay down, cached COPY . . (your code) changes daily — everything above it rebuilds. Keep it high. container's writable layer lost when the container dies older · more stable · deeper Read like rock: each build instruction deposits one stratum on the last.
Figure 14 — Layers as sediment An image is deposited bottom-up, one layer per instruction, and the cache survives only up to the first stratum that changed. Put the volatile material — your code — near the surface, and a day's edits disturb nothing beneath; bury it low and every build re-lays the whole bed.

Two more habits complete the literacy. Start slim: python:3.12-slim is a fraction of the full image's size; smaller images pull faster, cold-start faster, and carry fewer packages for a scanner to flag. Ship less: a .dockerignore file keeps .git, tests, and local clutter out of COPY . . — and for compiled languages, multi-stage builds discard the whole toolchain and ship only the binary.3 The 4GB image in this class's Situation 01 is what happens when none of these habits showed up.

§4

ACR — push, pull, scan

An image on your laptop helps nobody. Azure Container Registry is where Campux's images live: a private registry in your subscription, guarded by the same Entra ID and RBAC as everything else you own. The working loop is three verbs:

# create once
az acr create --resource-group rg-campux-platform \
  --name campuxregistry --sku Basic

# build in the cloud — no local Docker needed
az acr build --registry campuxregistry \
  --image pos-batch:1.0 .

# see what lives there
az acr repository list --name campuxregistry -o table

Three things in that loop deserve a second look. az acr build uploads your build context and builds the image in Azure — which means Cloud Shell, a locked-down laptop, or a hosted runner can produce images without a local Docker daemon, and it is how this class's Lab 2 works. Authentication is Class Nine's story again: humans run az acr login off their Entra identity, pipelines arrive through the Class Twenty-Three OIDC login, and the services that pull images at runtime do it with managed identities — an admin username and password exist as a legacy option, and leaving that switch off is the first thing a reviewer checks. And the registry is where scanning lives: Microsoft Defender for Cloud can inspect every pushed image against known vulnerabilities, which turns "we patch our servers" into the container-era question — when did we last rebuild our images? A container is never patched in place; it is rebuilt from an updated base and redeployed, and the registry is where that discipline is visible or absent.

§5

What containers don't solve

Containers arrive wrapped in enough enthusiasm that the honest list of what they do not do is a competitive advantage in interviews. They do not make code correct — the batch job's bugs ship in perfect fidelity to every environment now. They do not make software secure — a vulnerable library is exactly as vulnerable in a container, and §4's scanner exists precisely because images rot as CVEs are published against yesterday's layers. They do not manage state — the writable layer dies with the container, and databases, uploads, and anything precious still need the storage decisions of Class Twelve. And they do not run themselves: something must start containers, restart the crashed ones, scale them for November, route traffic to them, and feed them secrets. That something is called orchestration, and choosing Azure's version of it is the whole of next class.

Ship the machine with the work.

What they do solve, they solve completely: the environment problem is over. The artifact that passed the pipeline's tests is — byte for byte — the artifact that runs in production, and "works on my machine" becomes "works in the image, or does not work at all". That is a narrower promise than the enthusiasm suggests, and it is still one of the great bargains in the field: one skill, learned once, that every employer in the market currently assumes.

Case File · Campux Retail

The batch job, boxed at last

pos-batch:1.0 lands in campuxregistry

Campux's oldest running code is the POS reconciliation job — the nightly script that collects sales files from forty stores and settles them against the ledger. It has lived on one VM since before Class One, and it breaks in a specific, maddening way: whenever anything on that VM changes. A Python upgrade in March cost a weekend; a library patch in June silently rounded currency differently for two days. The job itself is fine. Its environment is a pet nobody dares touch — which is why the VM has not been patched since June, which is its own ticking bill.

This class it gets boxed. The §3 Dockerfile pins the runtime and every dependency; az acr build produces pos-batch:1.0 in campuxregistry; and the same image now runs identically on the engineer's laptop, in the pipeline's test step, and anywhere Azure can pull it. The store credentials that §Drill 04 would flag stay out of the image — supplied at runtime, per Class Nine. What the box does not yet have is a home: it still needs something to run it every night, scale it to zero the other twenty-three hours, and stop costing January money for November work. That decision — App Service, Container Apps, or the K-word — is next class, and it is the best cost story in the bootcamp.

Watch · Microsoft Learn

The official pages, and a CAMPUX overview

Docker's own overview first; the ACR page before Lab 2
Docs · Both halves

Docker Docs — What is Docker?
docs.docker.com/get-started/docker-overview/

Microsoft Learn — Introduction to Azure Container Registry
learn.microsoft.com/azure/container-registry/container-registry-intro

CAMPUX overview video

A container starting in under a second next to a VM booting, the Figure 14 layer cache saving a rebuild, and an image pushed to ACR and pulled somewhere else will live here. Video to be added.

Lab 1 · Image vs container

Run one image three times, break one copy

~15 minutes · Docker Desktop (docker.com/get-started) or any machine with Docker

Feel the §2 distinction with your hands: one frozen image, several disposable running copies, and a writable layer that dies on schedule.

  1. Pull an image, then prove it is only an artifact:

    docker pull nginx:alpine
    docker image ls
    docker ps
    What to notice: the image is listed (a few dozen megabytes), but docker ps shows nothing running. You own the recipe; no meal exists yet.
  2. Run it twice — two containers from one image:

    docker run -d --name web-a -p 8080:80 nginx:alpine
    docker run -d --name web-b -p 8081:80 nginx:alpine
    docker ps
    On screen: two containers, one image, each answering on its own port — open localhost:8080 and :8081 in a browser. Note how fast the second one started: the image was already local, so "boot" was just starting a process.
  3. Vandalise one copy — write into its filesystem:

    docker exec web-a sh -c \
      'echo "<h1>Campux was here</h1>" > /usr/share/nginx/html/index.html'
    On screen: localhost:8080 now shows your graffiti; :8081 is untouched. You wrote into web-a's writable layer — the image, and its sibling, never felt it.
  4. Now destroy and resurrect:

    docker rm -f web-a
    docker run -d --name web-a -p 8080:80 nginx:alpine
    docker rm -f web-a web-b   # cleanup when done
    The lesson: the reborn web-a serves the pristine page — the graffiti died with the writable layer, exactly as §2 promised. Anything that must survive a container belongs outside it. That one demonstration is most of container operations in miniature.
Note · if installing Docker Desktop is not an option on your machine, read the steps closely and run Lab 2 — it builds real images with no local Docker at all. Docker Desktop is free for individual learning use at the time of writing; check current licensing.
Lab 2 · Build & push to ACR

A real image in a real registry — no local Docker

~15 minutes · Cloud Shell · creates a Basic ACR (deleted at the end)

Build the batch-job image in the cloud with az acr build, list it, and watch the layer cache work — all from Cloud Shell.

  1. Create a registry (the name must be globally unique — adjust yours):

    az group create --name rg-acr-lab --location eastus
    az acr create --resource-group rg-acr-lab \
      --name campuxlab<initials> --sku Basic
    What to notice: Basic tier is the learning size — same API as Premium, smaller storage and limits. The registry is empty; az acr repository list returns nothing yet.
  2. Make a tiny application and its Dockerfile:

    mkdir batch-lab && cd batch-lab
    echo 'print("Reconciled 40 stores.")' > batch.py
    cat > Dockerfile <<'EOF'
    FROM python:3.12-slim
    WORKDIR /app
    COPY . .
    CMD ["python", "batch.py"]
    EOF
    What to notice: four instructions, four layers — a miniature of §3. No pip install because there are no dependencies; the shape is what matters.
  3. Build it in the cloud, twice:

    az acr build --registry campuxlab<initials> --image pos-batch:1.0 .
    az acr build --registry campuxlab<initials> --image pos-batch:1.1 .
    On screen: the first build downloads the Python base image and lays every layer; the second flies through with cache hits on the unchanged strata. You just watched Figure 14 happen in the log. Edit batch.py and build 1.2 to see exactly which layers rebuild.
  4. Inspect the registry, then clean up:

    az acr repository list --name campuxlab<initials> -o table
    az acr repository show-tags --name campuxlab<initials> \
      --repository pos-batch -o table
    az group delete --name rg-acr-lab --yes --no-wait
    The lesson: one repository, three tags, versioned like code — because it is code's sibling artifact. In the real pipeline this build step runs in Actions after the tests pass, and the tag it pushes is the exact artifact production will pull. Delete the group; a Basic registry bills modestly but bills.
On the job

The phrase retires

You · Cloud Engineer · "works on my machine"

A build passes locally and fails everywhere else, and the room shrugs "works on my machine." You end the argument by putting the app in a container — same image, same dependencies, everywhere — so the machine stops being a variable. The next environment gets the exact bytes you tested, and the phrase quietly retires.

Class Twenty-Six

Examination

Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.

Drill 01Recall · image vs container
What is the relationship between an image and a container?
Marked

B. Image to container is class to object, Bicep file to deployment: one precious definition, many disposable instances. C has the direction backwards — a surprisingly common interview stumble, and one that reveals whether someone has actually run docker run twice from one image. Getting the direction right matters operationally: you version, scan, and promote images; you start, kill, and never mourn containers. A team that patches a running container instead of rebuilding its image has recreated the pet VM with extra steps — Lab 1's graffiti demonstration is the antidote.

Drill 02Recall · the writable layer
A container has been writing customer receipts to its own filesystem for a week. The container is deleted and recreated from the same image. What happened to the receipts?
Marked

C — and this is the answer that costs real money to learn in production. The image is read-only; everything a container writes lands in its private writable layer, and that layer is deleted with the container. Nothing flows backwards into the image (B) or the registry (D) — images change only when you build them. The new container starts from the pristine image (which is why A feels plausible and is wrong: same image, yes; same writes, no). The design lesson: state that matters — receipts, databases, uploads — lives outside the container, in mounted volumes or the Class Twelve storage accounts. Containers are for compute; the moment one becomes the only home of data, you have built a pet that cannot even survive a restart.

Drill 03Select three
Which three genuinely travel inside a container image?
Marked

Code, runtime and libraries, and the base image's userland. The two rejects define the container's boundaries, one below and one above. Below: the kernel is the one thing containers share with their host — that sharing is the entire trick that makes them megabytes instead of gigabytes, and it is the honest answer to "how is this different from a VM?" (a VM ships its own kernel; a container borrows yours). Above: future runtime data cannot be in the image because the image is frozen at build time — writes land in the disposable writable layer, per Drill 02. Everything between those boundaries — the userland — travels. Hold the sandwich: kernel shared below, data external above, environment fully packaged between.

Drill 04Spot the error
This Dockerfile builds and runs perfectly. One line is a serious mistake. Which?
# Dockerfile — pos-batch
1.  FROM python:3.12-slim
2.  WORKDIR /app
3.  COPY requirements.txt .
4.  RUN pip install -r requirements.txt
5.  ENV STORE_API_KEY=sk-campux-pos-77413
6.  COPY . .
7.  CMD ["python", "batch.py"]
Marked

Line five. An ENV instruction writes the key into an image layer, and layers are not private: anyone who can pull the image can read it with docker history or docker inspect — no running container required. Push this image to a registry and the credential is now distributed to every machine that ever pulls it, held in every cache, and unremovable by publishing a fixed version, because the old layers still exist. It is the Git-history lesson of Classes Twenty and Twenty-One in a third costume: artifacts that are built to be copied are the worst possible vault.

The fix is the same as ever: the image carries the need for a key, never the key — supplied at runtime by the platform (an environment variable set by Container Apps, a Key Vault reference, a managed identity that makes the key unnecessary). The distractors are all healthy lines: slim is the right base (A), copying requirements early is the Figure 14 cache pattern working as intended (B), and that CMD is textbook (D). Interviewers love this drill's shape because the file works — the mistake is invisible to every test except the one that matters.

Situation 01Write before you reveal
A teammate's new service image is 4.2GB. Deploys are slow, cold starts are terrible, and the vulnerability scanner flags hundreds of packages nobody recognises. They shrug: "Disk is cheap." How do you interrogate the image, and what do you say to the shrug?
The image will confess its own history if you ask it. And the cost was never the disk.
Reasoning

Interrogate before opining — the image keeps records. docker history lists every layer with its size, which converts "it's huge" into "these three layers are 3.5GB of it". The usual suspects appear immediately: a full fat base image where slim would do; a COPY . . with no .dockerignore, hauling in .git, datasets, and local clutter; a compiler toolchain that built the app and then stayed for the ride; caches from package managers never cleaned in the same layer. Naming the specific layers matters — it turns a style argument into a bill with line items, and it takes five minutes.

Answer the shrug on its own terms: nobody was billing the disk. Size costs where the image moves — every deploy pulls those gigabytes to every node, so releases crawl and cold starts (which next class's scale-to-zero makes routine) take a minute instead of seconds. And the scanner noise is the sharper edge: hundreds of flagged packages the service never calls is not hypothetical risk, it is a standing tax — every CVE in that pile must be triaged by a human forever, and the real vulnerability that matters will drown in the noise of packages nobody can even name. A small image is not tidiness; it is faster releases and a security backlog someone can actually read.

Then make the fix boring and bounded. Swap to the slim base and rebuild — often half the size for one line. Add .dockerignore — five minutes. If a toolchain is riding along, split the build into two stages and ship only the output. Realistic result: 4.2GB to a few hundred megabytes in an afternoon, scanner findings cut by an order of magnitude, and no code changes at all. Offer to pair on it — the §3 habits stick best when someone watches docker history shrink in real time. "Disk is cheap" was true; it was also never what anyone was paying.

Situation 02Write before you reveal
After the POS job is containerized, a manager concludes: "Good — it's in a container now, so it's isolated and secure, and we can stop worrying about that old VM's patching problem." What is right and what is dangerously wrong in that sentence?
The patching duty didn't disappear. Find where it moved.
Reasoning

Concede what is genuinely right first. The environment-drift problem — the thing that actually broke the POS job twice — is solved, fully. No more Python upgrades rounding currency at 2am; the runtime is pinned in the image and reproduced exactly, everywhere, forever. And containers do provide real process and filesystem isolation. The manager's relief is earned on those points, and saying so buys the credibility for the correction that follows.

Then find where the patching duty moved, because it did not vanish. The image froze the environment — including its vulnerabilities. The OpenSSL in pos-batch:1.0 is the OpenSSL of the day it was built, and it ages exactly as fast as the VM's did; the difference is that nobody will ever patch it in place, because containers are not patched, they are rebuilt. "Stop worrying about patching" must become "rebuild the image on an updated base, on a schedule and on CVE alerts" — a pipeline job, per §4's scanner, not a hope. A team that containerizes and then stops rebuilding has swapped an unpatched pet VM for an unpatched frozen one, plus a false sense of security — strictly worse. And the isolation claim needs its §5 boundary: the kernel is shared, the job's own bugs shipped in perfect fidelity, and its credentials and network exposure are exactly as designed, no more.

Close by converting the worry, not cancelling it. The old worry was "what state is that VM in?" — unanswerable, per the drift lessons of the whole bootcamp. The new worry is "when was this image last rebuilt, and what does the scanner say?" — answerable in one registry query, automatable in one workflow. That is the honest win to hand the manager: not less responsibility, but responsibility that finally has a dashboard. Containers did not remove the duty; they made it legible, and legible duties are the ones that actually get done.

Examination record · first attempt
0/4
Class Twenty-Six · Complete
Retain this much

Five things worth carrying out of this class

  1. Containers end "works on my machine" by shipping the environment with the application — everything above the kernel, packaged; the kernel itself, shared with the host. That sharing is why they are megabytes and milliseconds where VMs are gigabytes and minutes.
  2. Image, container, registry: the frozen artifact, one running instance of it, and where artifacts live between machines. Version and scan images; start and discard containers; keep state outside both.
  3. A Dockerfile deposits layers like sediment, and the cache survives up to the first changed stratum — so volatile things (your code) go near the top, stable things (dependencies) near the bottom. One transposed COPY is a hundred slow builds a week.
  4. ACR is your registry: az acr build makes images without local Docker, Entra identities replace admin passwords, and Defender scanning turns "patching" into its container form — rebuild on an updated base, on a schedule.
  5. Containers solve the environment and nothing else: not correctness, not security, not state, not operations. The thing that runs, restarts, scales, and feeds them is orchestration — next class's decision.
Notes
  1. The container-versus-VM sizes here are directional, not gospel — a slim Python image runs to tens of megabytes and a VM image to gigabytes, but bloated containers (see Situation 01) and trim VMs both exist. The structural point survives any specific numbers: a VM virtualises hardware and boots an operating system; a container isolates processes and starts one. Treat "megabytes and milliseconds" as the honest order of magnitude, and measure your own images before quoting them.
  2. Docker the company, Docker the CLI, and containers the technology are three different things that share a name. The image format and runtime behaviour are open standards (OCI), Kubernetes and the Azure services run containers without Docker's engine, and Docker Desktop's licensing for larger companies has changed before and may again. The skills in this class transfer to every runtime; the tool you typed today is simply the most common on-ramp.
  3. Multi-stage builds deserve a fuller treatment than this class gives them: a first FROM compiles the application with the entire toolchain, a second FROM starts clean and copies in only the built output, and the toolchain never ships. For Python the win is modest; for Go, Rust, Java, or anything with a node_modules folder, it is routinely the difference between a 2GB image and a 40MB one. File it under "the first thing to reach for" when Situation 01 happens to you.