Docker — Images, Layers, Dockerfile, Networking
Understand image layers, build-cache invalidation, process signals, runtime isolation and service networking through versioned reference examples and explicit validation criteria.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- A Dockerfile EXPOSEs 8000 and copies site-packages but not console scripts. Why might the image start fail?
- Explain the version and execution boundaries before applying the examples
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 34 sections
Review and execution boundary
Linux container concepts; Dockerfile and Docker Engine 28+ reference instructions, no image builds or container starts. The Engine 28 boundary matters because older releases had an L2-access caveat for localhost-published ports.
Historical review notes are dated 7 October 2026; the current body was freshly reviewed on 10 October 2026 against saved primary-source passages. The review is bounded editorial correction, not certification of every dependency, security property or cloud deployment. Historical setup commands and optional exercises were not executed. No cloud resources, third-party packages or external side effects were created. Old screenshots and unavailable private assets remain in the private recovery archive, not prerequisites for this lesson.
module: M07 · Systems & Infrastructure sessionNo: 65 track: SYS minutes: 90 goal: Write a multi-stage Dockerfile that packages a Python app with a measured size budget, understand the layer cache, and network two containers together.
Why this session exists
Containers are a common packaging and deployment option; VMs, managed runtimes and bare-metal services also remain valid. Docker is the vocabulary of that world — image, layer, Dockerfile, registry, volume, network. Get fluent with those seven words in one session and every subsequent conversation about Kubernetes, CI/CD, and cloud deploys stops being mysterious.
title: You will be able to items: Explain image vs container, and why layers are the key to caching + small images. — Write a multi-stage Dockerfile that packages a Python app without claiming a universal size reduction. — Network two containers together using a user-defined bridge and DNS by name. — Diagnose the top-3 Docker footguns: mutable :latest tag, running as root, and bloated layer cache.
Prerequisites
- S001 · Dev environment — Docker is needed only for the optional native lab; reading and CPU-only models do not require a daemon.
- S060 · Linux fundamentals — filesystems, processes, users.
- S005 · Python basics — you know pip and virtual environments.
Pre-read and pacing
Choose roughly 25 minutes of pre-reading, not every item below. The 5/15/25/15/10-minute section estimates sum to 70 minutes; adding 25 minutes of pre-reading gives 95, so the original 90-minute label is approximate, not an exact total or measured timing. Video labels, channels and durations are inherited recommendations, not verified playback, popularity rankings or endorsements.
url: https://www.youtube.com/watch?v=Gjnup-PuquQ title: Docker in 100 Seconds channel: Fireship minutes: 2 why: Optional short container introduction; original Fireship title/channel and two-minute estimate are unverified.
url: https://www.youtube.com/watch?v=3c-iBn73dDE title: Docker Tutorial for Beginners · Full Course channel: TechWorld with Nana minutes: 130 why: An optional introductory course. Skip parts you know, focus on Dockerfile + networking.
url: https://www.youtube.com/watch?v=fqMOX6JJhGo title: Dockerfile Best Practices channel: Docker minutes: 22 why: Intended hands-on companion for Dockerfile layering and .dockerignore; the original “Docker” attribution and 22-minute duration are unverified, so use the official written reference for contracts.
url: https://docs.docker.com/build/building/best-practices/ title: Dockerfile Best Practices source: Docker docs · official minutes: 15 why: The canonical list of ‘do this, not that’ for Dockerfiles. Bookmark it.
url: https://jvns.ca/blog/2016/10/10/what-even-is-a-container/ title: What even is a container? source: Julia Evans · jvns.ca minutes: 12 why: Optional explanation of namespaces, cgroups and filesystem isolation; “best” was subjective, and this is not the security specification.
url: https://github.blog/2020-10-13-a-look-at-containerds-container-lifecycle/
title: A look at containerd's container lifecycle
source: GitHub Blog
minutes: 15
why: What actually happens when you docker run — image pull, layer unpack, container create, process exec. Demystifies the black box.
(a) Intuition · 5 min
Recipe, image and running instance
A Dockerfile is a recipe, an image is its packaged result, and a container is an instance using that result. Two chefs do not get identical cakes when ingredients and tools differ; pin and control build inputs before claiming reproducibility. An image contains a filesystem representation plus configuration, distributed by a registry such as Docker Hub, GHCR or ACR. Tags are names you can move; digests identify content.
Running a container adds processes and a writable layer to the image's reusable lower layers, under the selected isolation settings. Stopping it retains the container and writable layer; removing it removes that layer, not the separately stored image. The lunchbox analogy stops here: neither a Dockerfile nor an image is a running virtual machine.
The primitives that make containers work
- Namespaces give a process a scoped view of such resources as PIDs, network interfaces, mounts and hostname. Which namespaces are shared is a runtime choice, not an unconditional guarantee.
- cgroups account for and constrain resource consumption. CPU quota throttles; memory-limit pressure can trigger reclaim and OOM termination. These are not dedicated hardware or a VM boundary.
- Layered filesystems reuse read-only lower content and add per-container writable changes. Shared storage still has copy-up, writable-data, page-cache and metadata costs; the exact storage driver matters.
- Capabilities and seccomp constrain privileges and syscalls through different mechanisms. Docker's default retained capabilities include NET_RAW; drop unneeded capabilities explicitly. Neither mechanism proves isolation against all host-kernel vulnerabilities.
A quick history so the ecosystem makes sense
Container tooling combines older isolation primitives with standardized image/runtime formats. Distinguish the OCI image and runtime specifications, containerd as a runtime manager, runc as a low-level runtime, and BuildKit as a builder. Kubernetes can use different CRI runtimes; neither its architecture nor a fixed build-speed multiplier follows from a Docker timeline.
(b) Visual walkthrough — images and cache
Image = a stack of read-only layers + a writable top
Separate what is shipped from what changes while the application runs. The image layers describe the reusable filesystem; the writable layer belongs to a particular container instance.
Figure text equivalent — labels and relationships
- Node “Layer 1 · base OS (alpine, ~5 MB)” (square).
- Node “Layer 2 · python 3.12 (~30 MB)” (square).
- Node “Layer 3 · pip install deps (~40 MB)” (square).
- Node “Layer 4 · COPY app source (~1 MB)” (square).
- Node “Layer 5 · writable (tmp files, logs)” (square).
- Group “Image layers (illustrative sizes)” contains “Layer 1 · base OS (alpine, ~5 MB)”, “Layer 2 · python 3.12 (~30 MB)”, “Layer 3 · pip install deps (~40 MB)”, “Layer 4 · COPY app source (~1 MB)”.
- Group “Container (running)” contains “Layer 5 · writable (tmp files, logs)”.
- “Layer 1 · base OS (alpine, ~5 MB)” leads to “Layer 2 · python 3.12 (~30 MB)”. Line: normal; ends: arrow point.
- “Layer 2 · python 3.12 (~30 MB)” leads to “Layer 3 · pip install deps (~40 MB)”. Line: normal; ends: arrow point.
- “Layer 3 · pip install deps (~40 MB)” leads to “Layer 4 · COPY app source (~1 MB)”. Line: normal; ends: arrow point.
- “Layer 4 · COPY app source (~1 MB)” leads to “Layer 5 · writable (tmp files, logs)”. Line: normal; ends: arrow point.
Key property: OCI layer descriptors identify serialized layer blobs by a digest; image configuration also identifies the uncompressed layer changesets (DiffIDs). Compression can change the blob digest without changing the uncompressed changeset. If the instruction and relevant inputs for dependency installation match the build cache, the builder reuses the cached SHA and skips re-installing. Layer order matters — put slow, rarely-changing steps first.
The lifecycle: Dockerfile → image → container
docker build .: the builder evaluates instructions and their dependency graph. Filesystem-changing operations and configuration instructions have different effects.docker taganddocker push: attach a human-readable name and publish the artifact to a registry. Authorization and retention policies still apply.docker pull: retrieve the selected image; locally present content-addressed layers can be reused.docker run: prepare the root filesystem and configured namespace/cgroup controls, then start the effective ENTRYPOINT/CMD. An init wrapper can be PID 1 instead of the app.- Stop gracefully, then
docker rm: removal drops the stopped container and writable layer; it does not remove the image. Ordinarydocker rmdoes not itself stop a running container.
Dockerfile — the seven instructions you'll use daily
Consult the full reference for lifecycle, build and security instructions beyond this core:
| Instruction | Mechanism and common mistake |
|---|---|
| FROM | Select a base or start a stage. python:3.12-slim supplies Python and distribution userland; scratch supplies no userland. A static binary may still need certificates/timezone assets. |
| WORKDIR | Set/create the working directory for following relative operations. Explicit working directories avoid fragile shell cd assumptions. |
| COPY | Copy build-context content, or selected artifacts from another stage. Copy requirements before changing application source to separate cache dependencies. |
| RUN | Execute a build-time command in shell or exec form. Combine creation/cleanup in the same filesystem layer when you want intermediate files excluded; deleting them in a later layer does not erase earlier bytes. |
| ENV | Persist an environment default in image configuration, overridable at runtime. It is not a secret store. |
| EXPOSE | Document a container port; it creates neither listener nor host publication. |
| CMD / ENTRYPOINT | Supply the default command/arguments. Prefer exec form such as CMD ["python", "app.py"]; shell wrappers need signal forwarding or a final exec. Runtime overrides and --init affect the effective process tree. |
The four container networking modes
- Default bridge: ordinary containers on it can communicate by IP; automatic name resolution is not its normal behavior (legacy links are a separate feature). Host networking/NAT/firewall details remain platform-specific.
- User-defined bridge:
docker network create mynet, then--network=mynet, provides name/alias discovery. The app can reachredis:6379on that network without publishing Redis to the host. It is a useful default for this multi-container lab. - Host: shares the host network namespace, so listener ports are host ports and normal port remapping is not the design. This reduces network isolation; any performance benefit must be measured, not called zero overhead.
- None: leaves ordinary external network connectivity disabled, normally with loopback only. Useful for disconnected batch tasks, but it does not remove kernel, filesystem or mounted-socket attack surfaces.
The mental model to hold.
An image is a frozen filesystem + metadata. A container is a running process + a writable layer on top of an image. Everything else — networking, volumes, orchestration — is glue around those two ideas.
A frozen filesystem explains reproducible packaging, but not VM-style isolation. The distinction between an image and a running process is also the route back to the host kernel that containers share.
Isolation is not a virtual machine
Misconception: "A container is a lightweight virtual machine. It has its own kernel-ish environment, so it's isolated from the host the way a VM is."
truth: A container is a normal Linux process on the host kernel, with namespaces limiting what it can see and cgroups limiting what it can consume. Ordinary Linux containers share their Linux host kernel. Docker Desktop commonly places that Linux host inside a VM on macOS or Windows; sandboxed runtimes can introduce additional boundaries. Do not compare a Linux container kernel with the macOS kernel and expect equality.
why: The myth is sticky because the experience is convincingly VM-like: your own filesystem, your own process tree where your app is PID 1, your own network interface. Docker deliberately built that illusion because it is a useful abstraction. It matters when the illusion leaks — a container reading the host's CPU count instead of its cgroup limit, a kernel parameter that is global rather than per-container, or a privileged container that is effectively root on the host.
Reference inspection, not executed: compare the container's kernel with its Linux host (which may be a Docker Desktop VM), then inspect the relevant process namespace links and cgroup membership. Equal uname text alone does not prove isolation or identity; select the actual host PID and cgroup path.
# Reference only; no container, process or cgroup was created for this review.
uname -r
ps -ef | grep your-app
# Set PID to the inspected process; paths and permissions are host-specific.
ls -l /proc/$PID/ns/
cat /proc/$PID/cgroup
# cgroup v2 only, at the actual container cgroup/mount:
cat /sys/fs/cgroup/memory.maxPID 1, signals, mounts and resource boundaries
The runtime sends the configured stop signal (normally SIGTERM) to the container's main process, waits for the configured stop timeout, then uses SIGKILL if necessary. Ten seconds is a Linux default, not a universal deadline. Exec-form avoids an intermediate shell that might not forward signals; a shell entrypoint should exec its final process. Namespace PID 1 has special signal semantics and must reap adopted children; --init supplies a small forwarding/reaping init. It does not implement the application's request draining or repair an arbitrarily blocked handler. In this lab the init is PID 1 and Gunicorn is its child supervising workers.
A mount namespace changes the mount view; a bind mount can deliberately expose host files and is writable by default. Mount only required paths, read-only where suitable. A read-only root filesystem needs explicit writable paths for legitimate temporary/state files and does not make other writable mounts read-only. A named volume outlives ordinary container removal; its driver, backup and restore contract determine durability. Stopping and starting the same container retains its writable layer; removing and recreating it does not. Mounting over a populated directory obscures the image's files there rather than merging them.
Capabilities divide Linux privileges, seccomp restricts syscalls, user namespaces/rootless mode change UID mapping, and cgroups account/limit resources. None supplies a separate kernel. A rootful Docker daemon socket gives its authorized client powerful host control; even a non-root process with socket access can be dangerous. Conversely UID 0 in a user namespace is not automatically host UID 0. Dropping capabilities and forbidding privilege escalation reduce risk but do not prove confinement against kernel or application defects.
Worked resource decision. With a hypothetical --cpus=0.5 quota over a 100 ms period, a CPU-bound workload gets about 50 ms aggregate CPU time per period before quota throttling; this is not a dedicated half-core or a latency promise. A 128 MiB memory limit does not give 128 MiB exclusively to Python objects: charged memory can include additional process/kernel/cache costs. Read the actual cgroup and runtime metrics, account for swap configuration, and distinguish a memory-limit OOM from node-wide pressure or an external SIGKILL. Exit 137 alone is insufficient evidence.
# CPU arithmetic only; this does not configure or emulate cgroups.
def quota_microseconds(cpus, period_us=100_000):
if cpus <= 0 or period_us <= 0:
raise ValueError("positive quota inputs required")
return int(cpus * period_us)
assert quota_microseconds(0.5) == 50_000
assert quota_microseconds(2) == 200_000Deriving layer reuse
Why are Docker images built in layers rather than as one filesystem snapshot? Layers add complexity — what forces them? steps: premise: Images must be distributed over a network to many hosts, and images are large — often hundreds of megabytes to gigabytes.; because: they contain an entire userland: base OS, runtime, dependencies, and your application — premise: Most images share the vast majority of their content, because they derive from a handful of common base images and a small set of runtimes.; because: everyone builds on the same distributions and language images; only the top slice is unique — premise: Therefore transferring whole images would send the same bytes repeatedly to every host, wasting bandwidth proportional to the redundancy.; because: without a way to identify shared content, each transfer is independent and unaware of what the host already has — premise: Splitting the image into content-addressed layers lets a host fetch only the layers it lacks, and store one copy of a layer shared by many images.; because: content addressing makes identity a hash, so "do I already have this?" is answerable without trusting any name or version — premise: For layers to be shareable they must be immutable and stacked in a fixed order, with a union filesystem presenting the merged view and a writable layer on top at runtime.; because: a mutable layer could not be safely shared between images, and the stack must be deterministic for the hash to mean anything conclusion: Therefore layering is forced by distribution efficiency, and everything about Dockerfile authoring follows from it: filesystem-changing instructions create filesystem layers; metadata instructions update image configuration. Cache keys depend on instructions and their relevant inputs.And note what this predicts: instruction order determines build speed. Copying your source code before installing dependencies invalidates the dependency layer on every source change, so you reinstall everything on every build — which is exactly why the standard pattern is copy the manifest, install, then copy the source. It also predicts that deleting a file in a later layer cannot shrink the image, because the earlier layer still contains it and is still transferred. A secret written in one layer and deleted in the next is still in the image and still extractable. That surprises people constantly, and the derivation says it must be so.
Operational lifecycle model
A process wearing a costume picture: Take an ordinary process. Give it namespaces so it sees its own filesystem, its own PID 1, its own network stack, its own hostname. Give it cgroups so it can only use so much CPU and memory. That is a container — there is nothing else in the box.The image is a stack of read-only layers plus a thin writable layer created at start. Anything written to that top layer disappears when the container does, which is why persistence requires a volume. rules: Build order is cache order. Put what changes rarely at the bottom (base image, system packages, dependency manifests) and what changes constantly at the top (your source). Getting this backwards can rerun dependency installation; measure the actual build rather than promising a fixed speedup. — Use multi-stage builds to select runtime artifacts deliberately; Python source and dependencies still need to ship, and the chosen runtime base may already include tools. Fewer bytes can reduce uncached transfer work; startup latency and exploitability also depend on runtime behavior and dependencies. A compiler is one possible post-exploit tool, not the boundary that prevents exploitation. — Put replaceable scratch data in the writable layer and durable application state in deliberately managed volumes or external services. A stateful container is still a container; replacement safety depends on persistence, recovery and shutdown design. — Prefer one service responsibility per container; a Gunicorn master and its workers are multiple legitimate processes. The service must be PID 1-aware. Namespace PID 1 has special default-signal behavior; absent explicit handling/forwarding and child reaping, shutdown can time out and exited children can remain zombies. Use an init (--init, tini) or handle signals explicitly. trigger: Fire this model when you see: a build that takes minutes for a one-line change · a container that ignores its memory limit and gets OOM-killed · data disappearing on removal/recreation · a 2 GB image for a 20 MB binary · a shutdown that always takes exactly the grace period.
Choosing a base image
What base image do you build on: a full distribution, a slim variant, Alpine, or distroless? options: name: Full distribution (debian, ubuntu); gain: a familiar glibc environment and package tooling simplify debugging; verify the actual base contents and native dependency compatibility rather than assuming every tool is installed.; cost: hundreds of megabytes containing an enormous amount you never execute, which is both pull latency and CVE surface. Every unnecessary package adds maintenance surface; scanner findings depend on actual package versions and databases, not simply package presence.; pick: during development, or for applications with complex native dependencies where debugging time outweighs image size — name: Alpine; gain: small base and fewer packages can reduce transfer work and the components requiring patching. Measure actual pull/start time and assess vulnerabilities rather than deriving security from bytes alone.; cost: musl libc rather than glibc. Most things work, but the differences are real and surface at the worst time: DNS resolution behaves differently, some prebuilt binary wheels and native extensions do not work, and certain runtimes have historically shown different performance characteristics. You may spend more time on musl issues than you saved in bytes.; pick: simple statically-linked binaries — Go, Rust — where libc is barely involved — name: Distroless; gain: contains your application and its runtime dependencies and nothing else: no shell, no package manager, no utilities. Removing tools can reduce opportunities and scanner noise, but an attacker may still use the application runtime or supplied payloads. A distroless image is not an exploit-prevention guarantee.; cost: you cannot assume a shell is available; exec can still run a binary that exists in the image. You need a separate debug variant, or ephemeral debug containers, and your team must be comfortable diagnosing entirely through logs and metrics.; pick: production workloads for compiled languages, once your observability is good enough that shelling in is not your primary debugging method verdict: Compiled binaries: distroless in production, with a debug image available for incidents. Interpreted runtimes with native dependencies: the slim distribution variant, because the time lost to musl incompatibilities routinely exceeds the value of the smaller image.Reach for Alpine when the artefact is a static binary and the small base is nearly free. Do not choose it purely for image size on a Python or Node application with native extensions — that trade is worse than it looks, and the cost arrives as a confusing build failure at an inconvenient moment rather than as a line item you can see.
(c) Hands-on · 25 min
Reference exercise: compare a full-base development-server image with a slim-base, multi-stage Gunicorn image, then connect it to Redis through a user-defined bridge. Sizes are measured outputs, not promised 900/60 MB results. This compares several choices at once; isolate one variable if attributing a performance improvement. Use a disposable local Docker environment only. The historical package pins illustrate a reproducible input list, not current vulnerability approval; select supported patched versions and lock transitive dependencies before deployment.
#!/usr/bin/env bash
# s065-docker-demo.sh — from Dockerfile to a networked mini-app.
set -euo pipefail
DIR="$(mktemp -d -t s065.XXXXXXXX)"
cd "$DIR"
RUN="s065-$(date +%s)-$$"
APP_ID=""; REDIS_ID=""; NET_CREATED=""
cleanup() {
[ -z "$APP_ID" ] || docker rm -f "$APP_ID" >/dev/null
[ -z "$REDIS_ID" ] || docker rm -fv "$REDIS_ID" >/dev/null
[ -z "$NET_CREATED" ] || docker network rm "$RUN-net" >/dev/null
}
trap cleanup EXIT
log() { printf "\033[1;36m▶ %s\033[0m\n" "$*"; }
log "1/6 Writing app.py + requirements.txt"
cat > app.py <<'PY'
"""Tiny Flask app that increments a counter in Redis."""
import os
from flask import Flask, jsonify
import redis
app = Flask(__name__)
r = redis.Redis(host=os.environ.get("REDIS_HOST", "127.0.0.1"), port=6379, decode_responses=True)
@app.post("/hits")
def index():
hits = r.incr("hits")
return jsonify(hits=hits, host=os.uname().nodename)
@app.get("/health")
def health():
return "ok", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
PY
cat > requirements.txt <<REQ
flask==3.0.3
redis==5.0.7
gunicorn==22.0.0
REQ
log "2/6 Writing .dockerignore (keeps build context tiny)"
cat > .dockerignore <<'IGN'
__pycache__
*.pyc
.venv
.git
.gitignore
README.md
node_modules
.env
.env.*
*.pem
IGN
log "3/6 Writing the NAIVE Dockerfile (bad — for comparison)"
cat > Dockerfile.naive <<'DKR'
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
DKR
log "4/6 Writing the runtime-oriented Dockerfile (multi-stage, non-root)"
cat > Dockerfile <<'DKR'
# ---- 1. builder: install deps into a virtual environment ----
FROM python:3.12-slim AS builder
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install -r requirements.txt
# ---- 2. runtime: tiny image with just the venv + source ----
FROM python:3.12-slim AS runtime
# Non-root user for this application; review runtime permissions
RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin app
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY --chown=app:app app.py .
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
REDIS_HOST=redis
USER app
EXPOSE 8080
# gunicorn > flask dev server: real WSGI, N workers, sane signal handling
CMD ["gunicorn", "-b", "0.0.0.0:8080", "-w", "2", "--access-logfile", "-", "app:app"]
DKR
log "5/6 Building both images and comparing sizes"
docker build -f Dockerfile.naive -t "$RUN:naive" . 2>&1 | tail -3
docker build -f Dockerfile -t "$RUN:prod" . 2>&1 | tail -3
docker image inspect "$RUN:naive" "$RUN:prod" --format '{{.Id}} {{.Size}}'
log "6/6 Wiring it all up on a user-defined bridge"
docker network create "$RUN-net"
NET_CREATED=1
# Redis is unauthenticated only inside this isolated disposable lab network.
# Its anonymous storage is not a production persistence/backup design.
REDIS_ID="$(docker run -d --name "$RUN-redis" --network "$RUN-net" \
--network-alias redis redis:7-alpine)"
APP_ID="$(docker run -d --name "$RUN-app" --network "$RUN-net" \
--init --cap-drop ALL --security-opt no-new-privileges=true \
-p 127.0.0.1:8080:8080 "$RUN:prod")"
ready=0
for attempt in $(seq 1 30); do
if docker exec "$REDIS_ID" redis-cli ping | grep -q PONG && \
curl --fail --silent http://127.0.0.1:8080/health >/dev/null; then
ready=1; break
fi
sleep 1
done
[ "$ready" -eq 1 ] || { echo "lab startup timed out" >&2; exit 1; }
echo "--- three explicit writes ---"
for i in 1 2 3; do curl --fail --silent -X POST http://127.0.0.1:8080/hits; echo; done
# EXIT cleanup removes only containers/network created by this invocation.
# Keep the generated files/images for inspection; remove only these exact
# artifacts later after checking that no wanted data remains.What each block does
title: Anatomy of the Dockerfile The slim base removes packages relative to a full distribution variant, but it still contains Python and supporting libraries. The example installs into a complete virtual environment so console entrypoints such as gunicorn survive the stage copy. Both stages need compatible Python/OS/architecture; native wheels may require additional runtime shared libraries.
Copy the dependency manifest before source code to preserve installation cache reuse. The final base still includes pip; multi-stage does not automatically remove every build tool or vulnerability. USER app lowers privileges for this service, while runtime mounts, capabilities and the host kernel remain part of the threat model. Gunicorn supervises worker processes and handles termination; Flask's development server is not a production deployment choice (not because it is universally single-threaded).
.dockerignore keeps irrelevant and sensitive files out of the build context. Never COPY a secret and delete it later: an earlier layer may retain it. Use BuildKit secret mounts for build-time credentials, and avoid echoing them into output or persistent files.
title: What to observe
The naïve and runtime-oriented images must be measured with the same architecture and size metric. Registry compressed transfer size differs from local unpacked size.
Change app.py, rebuild both. The naïve ordering invalidates dependency installation; the runtime-oriented ordering can reuse that cache. Record timings on your own build host.
On a successful optional run, the three POSTs return incrementing hits, exercising app-to-Redis communication via its network alias.
docker exec "$APP_ID" id before cleanup shows a nonzero UID — not root. Compare to the naïve image where it's uid=0(root).
goal: See the layer cache in action
hint: Modify app.py, run docker build again, and watch how many layers say ‘CACHED’ vs need to be rebuilt.
Make a trivial change to app.py (add a comment). Then rebuild the production image with time docker build -f Dockerfile -t s065-app:prod . and observe:
- All layers up to
COPY --from=builder /opt/venv /opt/venvshould say CACHED in the build output. - Only the
COPY --chown=app:app app.py .layer (and everything after) needs to rebuild. - Compare uncached and cached timings; no fixed reduction is promised.
Now edit requirements.txt (add a version bump). Rebuild. Watch the pip install layer run again — because its input changed, its content hash changed, and the cache line broke. This is why ordering matters.
Bonus — docker exec and docker logs — your two debugging tools
docker logs -f "$APP_ID"before cleanup — tail the container's stdout+stderr. First place to look when something breaks.docker exec -it "$APP_ID" shbefore cleanup — get a shell INSIDE the running container. Poke at files, check env vars, run curl to test connectivity. Works on any container as long asshexists — for distroless / scratch images, you'll need a debug sidecar.
(d) Production reality · 15 min
scenario: Hypothetical failure analysis (not a verified incident) broke: A hypothetical CI fleet behind a shared NAT exhausts its registry pull allowance and receives rate-limit errors. Check the current registry policy and account limits rather than treating a historical numeric quota as universal. fix: Set up a pull-through cache (Nexus, Artifactory, or the built-in registry mirror), authenticate to Docker Hub with a paid account, or move base images to a private registry (GHCR, ECR, ACR). Digest pinning does not bypass pull quotas or avoid network access. Separately pin base images by digest — FROM python:3.12-slim@sha256:abc… — and cache the pull in your CI. lesson: Public registries are a shared resource. If your build pipeline pulls python:latest on every job, you're one policy change away from a production outage. Pin digests, cache locally.
scenario: Hypothetical failure analysis (not a verified incident) broke: A team ships myapp:latest to production. Wednesday's build introduces a bug. On Thursday, an unrelated pod restart pulls latest, gets Wednesday's image, and dies. Rolling back is unreliable if the previous digest was not recorded and retained; a moved tag alone does not identify the earlier image. fix: Never deploy :latest. Every deploy records an immutable digest plus a human-readable tag: git SHA, semver, or build number (myapp:v1.2.3, myapp:sha-a3f9c2). Roll back by selecting the previously recorded and retained digest. This is the single most important discipline in container ops. lesson: Tags are mutable pointers, digests are immutable. Use tags for humans; verify digests in production. Kubernetes imagePullPolicy: Always + :latest = you're playing Russian roulette with your uptime.
scenario: Hypothetical failure analysis (not a verified incident) broke: A container runs as root because the base image does and the Dockerfile never sets USER. A remote-code-execution bug in the app becomes root inside the container. An exploitable host-kernel bug or authorized access to a rootful daemon socket can then compromise the host; the impact depends on runtime and UID-mapping boundaries. fix: Every Dockerfile ends with a non-root USER. Runtime: enforce runAsNonRoot: true in Kubernetes SecurityContext, deny mounting /var/run/docker.sock, drop all capabilities except the ones the app actually needs. Adopt distroless images (no shell, no coreutils) for a smaller attack surface. lesson: Container isolation is a set of controls, not VM-equivalent assurance. Use a compatible non-root identity, least capabilities, carefully scoped mounts and read-only rootfs where possible; user namespaces/rootless runtimes alter the mapping, not the need for review.
Common footguns to internalise
- Anonymous volumes filling the disk — every
docker run -v /var/lib/foocreates a new anonymous volume. Inventory ownership and backups before removing any volume; do not prescribe broad prune commands on a shared host. - Zombie processes — inspect child ownership and reaping rather than blaming a language.
--initor a suitable init forwards signals and reaps adopted children; the service still owns its shutdown behavior. - Missing shutdown handling — inspect the configured stop signal and timeout. Docker escalates to SIGKILL after the timeout if the main process remains; test Gunicorn worker draining and its graceful timeout under real requests. A server name alone is not proof of graceful completion.
- Building on ARM Mac, deploying to x86 servers — the resulting image is ARM-only and fails on x86 with
exec format error. Fix:docker buildx build --platform linux/amd64,linux/arm64 --pushfor multi-arch images.
Where this shows up in the rest of the plan
root: Docker is the substrate for the next dozen sessions branches: label: S066 · Kubernetes basics; desc: Pods run containers. Everything you learned here about images and networking maps directly.; tone: violet — label: S067 · Kubernetes advanced; desc: ConfigMaps, Secrets, HPA — all operate on the containers you build here.; tone: cyan — label: S068 · Azure Cloud; desc: ACR (Azure Container Registry) hosts your images. AKS runs them.; tone: emerald — label: S069 · IaC · Terraform; desc: Terraform provisions the registry, cluster, and networking for your containers.; tone: amber — label: S078 · SRE · observability; desc: Container logs to stdout, Prometheus scrapes /metrics — patterns designed for this world.; tone: rose — label: Container security follow-up; desc: Image scanning, admission controllers, runtime detection — all layered on the primitives here.; tone: sky
(e) Recall + stretch · 10 min
Recall questions and solutions
Question: In one sentence, what's the difference between an image and a container?
Solution: An image is a frozen filesystem + metadata (built once, immutable). A container is a running process that uses an image as its rootfs with a thin writable layer on top.
Question: Why is layer ordering in a Dockerfile important?
Solution: The build cache is content-hash based. If layer N changes, all layers after N are invalidated. Put slow, rarely-changing steps first (base image, system deps) and fast, frequently-changing steps last (application source).
Question: Give one reason multi-stage builds produce smaller images.
Solution: The final image only copies specific artifacts from the builder stage. Only artifacts absent from the final base and final copy operations stay out of the runtime image. Measure the actual runtime artifact; reductions depend on the base and copied dependencies.
Question: What's the difference between CMD and ENTRYPOINT?
Solution: ENTRYPOINT supplies the default executable and can itself be overridden with --entrypoint; CMD is default arguments passed to it. ENTRYPOINT ["gunicorn"] + CMD ["app:app"] means docker run img other:app overrides the CMD but keeps gunicorn. Most apps just use CMD alone.
Question: Why should you avoid FROM python:latest in production?
Solution: latest is a mutable tag — the same tag can point to different images over time. Reproducibility is destroyed. An explicit version tag documents intent but can still move; pin and retain the reviewed digest (@sha256:…) to identify the image content. That does not freeze runtime configuration or external services.
Question: Explain the difference between a bind mount and a named volume.
Solution: A bind mount maps a host path into the container (-v /host/path:/container/path); development and production both require an explicit path, permission and lifecycle contract. A named volume is Docker-managed storage (-v mydata:/data) — managed by the selected volume driver; the default local driver does not migrate its data between hosts.
Question: What does the default bridge network give you, and why do you usually create a user-defined bridge instead?
Solution: Default bridge lets containers talk by IP but NOT by name (no DNS). A user-defined bridge (docker network create foo) adds an embedded DNS server so containers on that network resolve each other by name — essential for compose/multi-container apps.
Question: You run docker run myapp and the process dies immediately. How do you debug it?
Solution: docker logs \<id> shows the container's stdout/stderr. If empty, override the entrypoint: docker run --entrypoint sh -it myapp to get a shell and investigate. Most common causes: missing env vars, wrong CMD path, permission denied on a file.
Question: Why should containers not run as root?
Solution: Non-root reduces process privilege, but host impact depends on namespace mappings, capabilities, mounts and exploitable bugs. Access to a rootful daemon socket can defeat that reduction. Enforce an appropriate USER and runtime security policy; admission enforcement depends on configured policy, not a universal platform default.
Question: Stretch — Design the Dockerfile for a Go binary that needs to be as small as possible. Target: under 15 MB.
Solution: Multi-stage reference Dockerfile; replace the historical Go tag with your reviewed supported version and digest:
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]Measure the final image rather than asserting a 10 MB result: it contains the static binary and selected distroless runtime assets. Removing a shell and unused libraries reduces some components; it does not establish a minimum or eliminate application/kernel vulnerabilities. Go's static compilation + CGO_ENABLED=0 is what makes this possible.
Explain-out-loud test
- What are the Linux primitives that make a container a container?
- Why does the order of instructions in a Dockerfile matter?
- Give three rules for a production-oriented Docker image. (multi-stage · non-root · pinned base · explicit tag)
What comes next
Previous: S064 · CDN. Next: S066 · Kubernetes basics.
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: an estimated 90 minutes, including selected pre-reading; optional native labs may take longer.
Corrected contracts and failure analysis
Images are content-addressed filesystem/configuration artifacts; containers add processes and a writable layer. EXPOSE documents a port but does not publish it. Publish local experiments to 127.0.0.1 explicitly. A container is not a VM security boundary: it shares the kernel, and root/capabilities/mounts change the isolation risk.
Exit 137 indicates SIGKILL-style termination, not proof of an OOM. Inspect the runtime OOMKilled state and host events. Multi-stage builds omit build tools only if the final stage does not copy them back. Native Python extensions may still need runtime shared libraries. A smaller image is not automatically safer, and non-root does not eliminate application vulnerabilities. A HEALTHCHECK records health for the runtime; an orchestrator may require its own independent probes.
Boundary exercise with solution
A Dockerfile EXPOSEs 8000 and copies site-packages but not console scripts. Why might the image start fail?
Solution and reasoning
EXPOSE does not make uvicorn available or publish the port. Use a complete installed prefix or python -m uvicorn and ensure dependencies are in the final image. Configure host port publication separately and use a non-root user. Inspect, do not infer, the final artifact.
Source-backed review notes
- Multi-stage builds | Docker Docs — accessed 2026-10-07. Exact supporting passage: “With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image.”
Packaging and networking assessment
The original under-100-MB goal is a budget to test, not an API guarantee. A compressed registry image below that threshold can exceed it unpacked, and a supported Python base plus native libraries may make the target inappropriate. A successful design explains the measured size and retained dependencies rather than removing required libraries to satisfy a slogan.
Paper exercise. The base and requirements do not change; only app.py changes. Which installation step needs to rerun? Then a credential is copied in an early layer and deleted in a later one: is the pushed image safe?
Solution. The dependency installation can reuse its cache because COPY requirements and its relevant inputs are unchanged; the application COPY and dependent steps are reconsidered. A later deletion does not erase a credential from an earlier layer. Rebuild without the secret and rotate any exposed credential; shrinking the final visible filesystem is not a security repair.
Network trace. The host connects to 127.0.0.1:8080, forwarded to the app's port 8080. Inside the app container, Redis is reached by the network alias redis on port 6379, not by localhost. No Redis host port is published. EXPOSE documents a container port but creates neither listener nor host mapping. The app's /health only proves process liveness; POST /hits proves the Redis-dependent path. Stop/remove/recreate tests distinguish the disposable counter from a durable storage design.
Acceptance checklist. Inspect the nonroot UID, effective command and digest; verify the copied entrypoint exists; exercise success, unavailable Redis and bounded startup timeout; measure image sizes/cache reuse; confirm host binding is loopback and cleanup targets only this lab's IDs. These are reference-design criteria, not a claim that the images ran during editorial review.
Recovery source checks
- Port publishing and mapping | Docker Docs: Explicit loopback publication restricts host exposure, with an old-engine caveat.
- Bridge network driver | Docker Docs: User-defined bridges resolve container names and aliases.
- Storage | Docker Docs: Writable container data disappears on removal, not mere stopping.
- Running containers | Docker Docs: Docker capability configuration can permit raw sockets.
- Build cache invalidation | Docker Docs: Build-cache lookup depends on instructions and relevant input metadata.
- Build secrets | Docker Docs: Build credentials should use secret/SSH mounts instead of persistent image inputs.
- Volumes | Docker Docs: Anonymous volumes survive container removal unless explicitly removed by supported lifecycle options.
Current source and verification boundary
Fresh body review: 10 October 2026. The following official primary sources were saved as raw and canonical bytes with exact supporting quotations in the batch-37 evidence packet. Docker documentation is pinned to a repository commit, not a claim that every Engine 28+ release behaves identically; Kubernetes sources are pinned to the release-1.34 documentation branch commit, not to a running cluster. RFC numbers and the OCI/Varnish tags identify their respective contracts. Historical package/image tags in the examples remain reference inputs, not current vulnerability approval.
- oci-layers — OCI image-spec v1.1.1.
- docker-cache — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-stop — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-run — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-storage — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-resources — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-multistage — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-publish — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
- docker-bridge — 18bbfeeb249da011f359d558dba84c4b6dc3a335.
Only the stated CPU models and static source/syntax checks are evidence from this review. No container, image build, Varnish listener, provider purge, Kubernetes cluster, browser application rendering or site deployment was executed. Preserved resource links are reading recommendations; videos and unverified historical pages are not evidence of a runtime result.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.