Skip to content
CAMPUX Cloud Bootcamp Phase Four · Class Twenty-Six
Phase Four — Operate, Secure & AI
4 drills · 2 situations · 2 Labs
Aligned to AZ-104
Class Twenty-Six

Containers & Docker

A container image packages an application with its runtime and libraries, so the same build can move from a laptop to a server. It still needs a compatible host, configuration and access to its external services.

Phase Four · Operate, Secure, AI · Class 26 of 42 · 22 min · No earlier classes

After this you can build an image, run a container and explain what a container is not.

The hands-on part also needs Docker Desktop or another container runtime.

§1

The problem, priced

Suppose an application runs on a developer's laptop with Python 3.12 and a library installed months ago. The server has Python 3.9 and lacks that library. Copying the source code to the server does not copy the environment the code needs. A container image packages the code, runtime, and installed libraries into a build that can be tested and shipped together.

A virtual machine includes a guest operating system and its own kernel. A container shares the kernel of the machine running it while carrying the application files above that layer. This often reduces startup and image overhead, but size and speed depend on the workload. A Linux image needs a compatible Linux environment and CPU architecture; Docker Desktop may provide that through a VM. The image also cannot package a production database, cloud permissions, or the network it will use.1

Container
An isolated running process started from an image. The image provides application files and dependencies; the container shares a compatible host kernel and receives runtime configuration, network access, and storage from its environment.
Figure — Two stacks, one kernel
containers app A its libs + runtime app B its libs + runtime container runtime host OS — one kernel hardware virtual machines app A + libs app B + libs guest OS its own kernel guest OS its own kernel hypervisor hardware one kernel, shared — the whole trick
The key distinction is the kernel: containers on a host share it; each VM has a guest kernel. Real image sizes and startup times vary, and many cloud containers run on VMs underneath. The figure isolates the architecture, not a guaranteed speed or size difference.
§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.

A tag such as pos-batch:1.4 is a movable label: a later push can make it point to a different image. A digest identifies content in the registry. A deployment that uses pos-batch@sha256:… selects a specific image manifest, provided that manifest remains available in the registry. Use tags to communicate versions and a verified digest when a deployment must not change because someone repushed a tag. Also select the correct platform: the same tag may refer to different architecture-specific images.

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

An image is built from a Dockerfile: ordered instructions that select a base image, add files, install dependencies, and set the startup command. Build tools can reuse work from earlier instructions when their inputs have not changed. Here is a small Python example:

# 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 order matters when a build cache is available. Copy requirements.txt and install packages before copying frequently edited application code. Then a code-only edit can reuse the dependency step. If COPY . . came first, that edit would invalidate the cache for the install that follows. A new builder or refreshed base image may still run the install again. Pin dependencies in requirements.txt and review base-image updates; python:3.12-slim is a moving tag, not a fixed digest.

Figure 1 — 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 1 — Layers as sediment The diagram shows build-cache order, not a literal layer for every instruction. Keep frequently changed code after dependency installation so a code edit need not invalidate that earlier step when the cache is available.

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.

Multi-stage builds — the first thing to reach for

The mechanism is two FROM lines in one file. Stage one installs dependencies and builds the site; stage two starts from nginx and copies only the generated site files:

# stage one — build with the full SDK
FROM node:24 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# stage two — ship without it
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

Only the final nginx stage is published, so node_modules and the build toolchain from stage one are absent from that image. This pattern fits a static site whose build writes to /app/dist; a server-rendered Node application needs a different runtime stage. Add a .dockerignore as well, so secrets and local clutter are not sent in the build context. Measure the resulting image instead of assuming a fixed size reduction.

Play

Play it through

Three minutes, two drags and a choice. Watch a library installed on a laptop go missing in production, then bake it into the image and ship that instead. It plays on its own and stops when it needs your hands.

Run one for real. Container Apps and ACR. Push an image to a registry with the admin account disabled, pull it with a managed identity, and watch the app scale to zero when nobody is calling it. About thirty-five minutes.

§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:

# illustrative commands; the group and globally unique registry name must exist
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

az acr build sends the build context to Azure for a remote build, so local Docker is not required. The registry name must be globally unique, and the identity running the command needs permission to build and push. A pipeline can authenticate to Azure with OIDC; a runtime service can be granted permission to pull with managed identity. Keep the registry admin account disabled when those identities meet the need. Vulnerability scanning requires an enabled security capability and appropriate configuration; merely creating a Basic registry does not prove images are scanned. Rebuild from patched dependencies and redeploy after testing rather than modifying a running container.

§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.

Images reduce one major source of drift: the code and dependencies tested together can be deployed together. To make that true, promote the tested image by digest instead of rebuilding it for production. The running result can still differ when host architecture, environment variables, secrets, storage, networking, or external services differ. Test those boundaries too.

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.

In this fictional case, the team builds pos-batch:1.0 into an image and tests that image before deployment. The store credentials stay out of it and are supplied at runtime. The same digest can be promoted to a compatible production environment, but the job still needs access to its sales files, ledger, and identity. The next class compares services that can run it on a schedule.

Fig. 2 · The batch job, boxed — one image, pulled everywhere
Dockerfile pins the runtime + every dependency pos-batch:1.0 immutable layers built by az acr build no credentials baked in campuxregistry the registry (ACR) the same image, pulled identically engineer's laptop · pipeline test step · anywhere Azure runs it build push pull promote the tested digest to a compatible host
The Dockerfile produces an image with application dependencies. Promote the tested digest to a compatible host; supply credentials, network access, and persistent storage at runtime. The image reduces dependency drift but does not make those external requirements disappear.
Watch · Microsoft Learn

Official references for the labs

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

Read Docker's overview before Lab 1 and Microsoft's ACR guide before creating a registry in Lab 2. The lab logs will show what actually happened in your environment.

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

You need Docker running on your machine first; docker run --rm hello-world should print a greeting. If it does not, see lab setup, or skip to Lab 2, which builds in Azure instead. This lab uses a published nginx image to show the difference between an image and two running containers. You do not build the POS Dockerfile here. Run the commands on a machine with Docker, with ports 8080 and 8081 free.

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

    docker pull nginx:alpine
    docker image ls
    docker ps
    What to notice: the nginx image appears under docker image ls. docker ps may show unrelated containers on your machine; it should not show one from this lab 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 from one image, each answering on its own port — open localhost:8080 and localhost:8081. If a name or port is already in use, choose another before running these commands; do not remove an existing container you do not own.
  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 Docker is unavailable, study the commands and use Lab 2 only if you have an Azure subscription and accept its registry/build charges. Check Docker's current Desktop licensing before installing it for an organisation.
Lab 2 · Second level · Build & push to ACR

A real image in a real registry — no local Docker

~15 minutes · Bash in Cloud Shell · creates a billable Basic ACR

This optional lab creates a registry and runs cloud builds. Registry storage and build execution may incur charges until cleanup. Confirm your subscription and permissions first. The build logs may show cache reuse, but a second remote build is not guaranteed to reuse the first build's layers.

  1. In one Bash session, confirm the intended subscription, then create a uniquely named resource group and registry. Keep the printed names for cleanup:

    az account show --query '{name:name,subscriptionId:id}' -o table
    LAB_RG="rg-acr-lab-$RANDOM-$RANDOM"
    ACR_NAME="cx26${RANDOM}${RANDOM}"
    printf 'Group: %s\nRegistry: %s\n' "$LAB_RG" "$ACR_NAME"
    az acr check-name --name "$ACR_NAME" --query nameAvailable -o tsv
    az group create --name "$LAB_RG" --location eastus
    az acr create --resource-group "$LAB_RG" \
      --name "$ACR_NAME" --sku Basic
    What to notice: check-name should print true. If it does not, choose another registry name before creating anything. Basic is a billable tier with its own limits. If registry creation fails after the group was created, inspect and remove that lab group.
  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: this tiny app has no third-party Python packages, so there is no pip install step. Dockerfile instructions control the build, but the number of instructions is not a reliable count of stored filesystem layers.
  3. Build it in the cloud, twice:

    az acr build --registry "$ACR_NAME" --image pos-batch:1.0 .
    az acr build --registry "$ACR_NAME" --image pos-batch:1.1 .
    On screen: compare both build logs and durations. A remote builder may reuse layers, pull them again, or start with a fresh cache. The important confirmed result is that two tags now reference images in your registry. If you edit batch.py and build 1.2, expect a third tag only after that optional build succeeds.
  4. Inspect the registry, then clean up:

    az acr repository list --name "$ACR_NAME" -o table
    az acr repository show-tags --name "$ACR_NAME" \
      --repository pos-batch -o table
    printf 'Delete lab group: %s\n' "$LAB_RG"
    az group show --name "$LAB_RG" --query '{name:name,resourceId:id}' -o table
    az group delete --name "$LAB_RG" --yes
    az group exists --name "$LAB_RG"
    The lesson: after two successful builds, expect one repository with two tags. The tag alone does not prove which image a production deployment pulled; record the tested digest for that. Confirm the group is the one this lab created before deleting it. az group exists should print false when cleanup completes.
Think in systems

Zoom out: an image is an environment you froze — for better and worse

You can write a Dockerfile and push to a registry. Now reason about what the image contains, what the host supplies, and what must be checked before deployment.

Feedback loops

An image can carry a vulnerable library into every compatible environment. What scans and rebuilds the image when that library needs a fix?

Dependencies & coupling

Everything that runs the image depends on what you baked in; a base image’s flaw is inherited by everyone downstream who never chose it.

The constraint

The value is reproducibility; the ceiling is discipline — credentials kept out, layers kept lean, the base kept patched — none of which the format enforces for you.

Second-order effects

"It’s just a container" invites baking a secret in for convenience, and now the secret ships to every registry puller — the exposure scales with the image’s usefulness.

What breaks when it scales

One un-pinned base tag means yesterday’s build and today’s differ invisibly; immutable, digest-pinned images are what make "the same bytes" actually true once many people build.

The engineer who ships is asked "does it build?" The engineer who gets promoted is asked "and what did you freeze into it?" — and kept the secrets out.

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 base-image userland files. The host supplies the kernel, so the image must match the target operating system and CPU architecture. Data written after a container starts is not part of the original image; keep anything that must survive in external storage. Runtime settings and external services are also outside the image.

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 1 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 change what the evidence identifies. Try a smaller supported base image, exclude unnecessary build-context files with .dockerignore, and use a separate runtime stage if build tools need not ship. Rebuild, test behavior, compare image size and scanner findings, then measure pull and startup times. A smaller image is plausible; the amount of improvement cannot be predicted from the 4.2GB total alone.

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. Images carry application files and dependencies, while containers share a compatible host kernel. CPU architecture, runtime configuration, storage, network access, and external services still matter.
  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. When a build cache is available, copying dependency manifests before application code can avoid reinstalling packages after a code-only edit.
  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 structural distinction is that containers share a compatible host kernel while VMs include guest kernels. Image size and startup time vary by workload, platform, and runtime. Measure your own deployment rather than using a universal megabytes-versus-gigabytes rule. ↩
  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. The multi-stage win varies by language, so distrust any single number. For Python it is modest — the runtime must ship either way; for Go, Rust, Java, or anything with a node_modules folder, it is routinely the difference between a 2GB image and a 40MB one. Measure your own images before and after, and quote those. ↩