How to explain to students
Open with the question: "Have you ever shipped your laptop's code to a server and watched it break?" That's the problem Docker solves. A container packages the app plus its OS-level dependencies into a single portable unit that runs identically on your laptop, your colleague's, and on AWS.
Use the analogy: "VMs are houses — full kitchen, plumbing, electricity. Containers are apartments — they share the building's plumbing (the kernel) but each has its own walls." A VM boots a full OS in minutes; a container starts in milliseconds and uses ~10× less RAM.
🎯 Practice Questions
Show Answer
Where it matters: running legacy software that requires a specific kernel version, kernel-module testing, or hosting Windows workloads on a Linux fleet. For those, VMs (or hybrids like Firecracker) are still the right tool — Docker is not a universal replacement.
docker run -d -p 8080:80 nginx on your machine. Open localhost:8080 in a browser. What does the -d flag do, and what would change if you removed it?How to explain to students
There are three pieces: the Docker daemon (long-running background process), the Docker client (the docker CLI you type into), and a registry (like Docker Hub or AWS ECR) that stores images. The CLI talks to the daemon over a socket; the daemon does the actual work of pulling, building, and running.
A Dockerfile is a recipe — each instruction creates a layer, and Docker caches layers aggressively. If you understand "order your Dockerfile from least-changing to most-changing", you've already mastered 80% of build performance.
node:20-alpine, never node:latest. Reproducibility > convenience.node_modules, .git, .env — never ship them into the image.Practical: Multi-stage builds shrink images dramatically
A naive Node image ships the whole toolchain (gcc, npm, build tools). A multi-stage build compiles in a "builder" stage, then copies only the runtime artefacts to a tiny final stage.
🎯 Practice Questions
COPY . . as its first instruction. Rewrite it so changing a single source file doesn't bust the npm install cache.CMD ["npm","start"] and ENTRYPOINT ["npm","start"]? Give a scenario where each is the right choice.Show Answer
docker run time. docker run myapp test would replace npm start with test.ENTRYPOINT sets the fixed executable. Anything passed to
docker run becomes arguments to ENTRYPOINT, not a replacement.Use CMD when the image is a generic runtime someone might invoke differently (e.g. a CLI tool image). Use ENTRYPOINT when the image must always run as the same process — typical for application containers. The combo
ENTRYPOINT ["node"] CMD ["server.js"] is a clean idiom.
node:20-alpine typically a better choice than node:20 for production images? What's one scenario where Alpine causes problems?.dockerignore for a Node.js project that excludes everything that should never end up in the image. List at least six entries.How to explain to students
An image is the blueprint; a container is a running instance of that blueprint. You can run 100 containers from one image. Tags are how you version images — and :latest is a trap, not a version. Use semver (:1.4.2) or git SHAs (:abc123f) so you always know exactly what's running.
A registry is the warehouse — Docker Hub for public images, AWS ECR for private images you don't want the world to pull. You log in once, then docker push uploads your image and docker pull downloads it on the server.
Practical: Pushing to AWS ECR (private registry)
ECR is AWS's private registry. The flow has one extra step — get a temporary token from AWS, hand it to docker login. After that, it's just docker push like any registry.
🎯 Practice Questions
:latest to production a recipe for incident? Describe a concrete failure mode.Show Answer
:latest is just a pointer; it gets overwritten every time someone pushes. Two problems follow:1. You can't reproduce yesterday's deploy. If
:latest on the server points to image SHA abc but the registry now has :latest pointing to def, a re-pull silently changes what's running.2. You can't roll back. "Deploy the previous
:latest" is meaningless — the previous image may be unreachable.Fix: tag with semver (
:1.4.2) or the git SHA. Use :latest only as a convenience pointer for local dev.
myapp:1.0.0 for both Docker Hub (under your username) and an ECR URI. Write the two docker tag commands.aws ecr get-login-password | docker login command worked yesterday but fails today with "no basic auth credentials". Why?How to explain to students
By default, every container gets an isolated network. Containers on the same custom bridge network can find each other by name — no IP gymnastics. This is how a web container reaches a database container: postgres://db:5432, where db is just the container name.
Containers are ephemeral — when one dies, its filesystem dies too. Volumes survive restarts and rebuilds. Use a named volume for database data, and a bind mount only for local development (mapping host code into the container for live reload).
docker rm.docker volume prune periodically — they leak disk over time.🎯 Practice Questions
Show Answer
bridge) is the legacy network and does not ship Docker's embedded DNS. Custom bridge networks do.Fix: create a custom network and attach both containers to it.
docker network create app-net → then run each container with --network app-net. They can now address each other by container name (http://web:3000, postgres://db:5432).
docker rm postgres and the database is gone. What should the original setup have done to prevent this from destroying the data?docker run flag combination that mounts your current working directory into /app inside a Node container read-only. Why might read-only matter?:ro suffix on a volume mount.How to explain to students
Running 4 docker run commands by hand to spin up app + db + cache + queue is fragile and unreproducible. Docker Compose describes the whole stack in one compose.yaml file — services, networks, volumes — and brings it up with docker compose up.
Compose is the standard for local development and small production deploys. For larger scale you graduate to Kubernetes, but every K8s engineer started with Compose.
🎯 Practice Questions
compose.yaml for a Node web app that depends on a Postgres database. Use environment variables for DB credentials and a named volume for Postgres data.depends_on and a healthcheck? Why is depends_on alone often not enough?Show Answer
depends_on only waits for the dependency container to start — not for the service inside to be ready. Postgres takes a few seconds to accept connections after the container starts. The web container can crash trying to connect during that window.Fix: add a
healthcheck on the dependency, then use depends_on with a condition:depends_on: { db: { condition: service_healthy } }Now the web container only starts once
pg_isready succeeds.
docker compose down and your database data is gone. What did Compose do, and what flag would have saved the data?up?How to explain to students
A container that runs as root is a one-line bug away from being root on the host. The single biggest container-security win is to add a USER directive and run as a non-privileged user. Most "Docker is insecure" headlines trace back to this one mistake.
After non-root, the next pillars are: scan images for known vulnerabilities (Trivy, Grype, Snyk), use minimal base images (alpine, distroless), never bake secrets into images, and pin your dependencies.
--env-file, Docker secrets, or AWS Secrets Manager at runtime.🎯 Practice Questions
CMD ["node", "server.js"] and no USER directive. Explain in one sentence what the running container's UID is, and why that's a security problem.Show Answer
USER directive, the container runs as UID 0 (root) by default. If the Node process is exploited (RCE in a dependency, mounted volume escape), the attacker gets root inside the container — and depending on host setup, can pivot to the host (e.g. via mounted Docker socket, host-shared volumes, or a kernel exploit).Fix: add
RUN addgroup -S app && adduser -S app -G app then USER app in the Dockerfile.
ENV STRIPE_KEY=sk_live_… in the Dockerfile so deploys are simpler. Three things now go wrong — list them.docker run --read-only --tmpfs /tmp myapp actually prevent? Name one app type where this isn't realistic.How to explain to students
Once your image is in ECR, AWS gives you several ways to run it: ECS Fargate (serverless containers, you don't manage servers), ECS on EC2 (you manage the EC2 fleet), or Elastic Beanstalk (the simplest — paste your image URI and click deploy). For first projects, Beanstalk's "Docker single-container" platform is the lowest-friction path.
Whichever you pick, the core flow is the same: (1) push image to ECR, (2) describe how to run it (task definition / Beanstalk config), (3) tell AWS to deploy it. Logs go to CloudWatch by default; you'll need an IAM role attached to the running task.
Practical: The lazy-but-fine path — Elastic Beanstalk
For a first deploy, Beanstalk's Docker platform is hard to beat. You provide a Dockerrun.aws.json pointing to your ECR image; Beanstalk handles the EC2, load balancer, autoscaling, and CloudWatch logs.
🎯 Practice Questions
Show Answer
Move to ECS Fargate when you need: multiple containers per service, finer task-role IAM, custom networking, or autoscaling on container metrics. The mental model is heavier (clusters, services, task definitions) but the control is worth it at scale.
Skip self-managed ECS-on-EC2 unless you have specific GPU/CPU constraints — Fargate's per-vCPU pricing is fine for early-stage workloads.
docker compose up but crashes in Fargate. List four common reasons (hint: networking, env vars, ports, IAM).How to explain to students
Dockerfiles are the perfect task for AI assistance: small files, well-defined patterns, lots of training data. But the same is true for AI's failure modes — it'll happily generate a Dockerfile that runs as root, exposes secrets, or pulls a vulnerable base image. The skill is in the prompt and the verification.
Treat AI output like a pull request from a smart-but-junior engineer: read it line by line, build it, scan it, then ship it.
🎯 Practice Questions
FROM ubuntu:latest and ends with CMD bash. List three concrete red flags before you run it.Show Answer
:latest tag — non-reproducible. Should be ubuntu:22.04 or similar pinned version.2. Ubuntu base — heavy (~70 MB before any app). Alpine, distroless, or
ubuntu:22.04-slim are slimmer + smaller attack surface.3. No
USER directive — runs as root. No HEALTHCHECK, no .dockerignore mentioned, and CMD bash isn't an application — it's an interactive shell, useless as a service entrypoint.Fix: pin the base, switch to slim/distroless, add a non-root user, replace the CMD with the actual app's entrypoint.
How to explain to students
This pulls the whole module together. Walk through it line by line on screen first, then ask students to recreate it from memory. The artefacts they leave with — a working multi-stage Dockerfile, a Compose stack, an ECR push — are exactly what a junior DevOps role expects.
HEALTHCHECK in the Dockerfile so orchestrators know when it's ready.Sample quiz questions (interactive)
docker run time?npm install?links: entries--link on docker run--cap-drop=ALLUSER directive (run as non-root)Fill-in-the-command
myapp:1.0?How to explain to students
Frame as a real on-call task: "Our nightly ECS deploys are slow because our images are 1.4 GB. Cut them in half by Friday — without changing app behaviour." This forces students to apply multi-stage builds, slim base images, layer ordering, and .dockerignore all at once.
📋 Assignment Requirements
- Start from the provided "naive" Dockerfile (single stage,
node:20,COPY . .first) - Refactor to a multi-stage build (builder + runtime)
- Switch to a slim base image (
alpine,slim, ordistroless) where viable - Add a
.dockerignoreexcludingnode_modules,.git,.env, tests, and docs - Add a
USERdirective — final container must NOT run as root - Add a
HEALTHCHECKhitting/healthz - Result must be ≥ 50% smaller than the starting image (proven with
docker images) - Functional test:
curl localhost:3000/healthzstill returns 200 - Bonus: Add a Trivy scan to your local build script that fails on HIGH/CRITICAL
- Bonus: Push the optimised image to a free-tier Docker Hub account, share the link
--from=builder, missed --omit=dev, ran as root, broke healthcheck path.