1 🍱 Why containers — the end of "works on my machine"
Pack the app WITH everything it needs; every machine opens the identical box.
🧑💻 your laptop: works!
node 20 installed, all libs there
💥 teammate's laptop: crash
node 14, missing libs, wrong path
1
the naked-app problem
🍱 the container
app + node 20 + libs + config
everything INSIDE the box
(shares the host's kernel —
way lighter than a VM)
2
💻 laptop A ✅
💻 laptop B ✅
☁️ AWS server ✅
3
identical, everywhere
2 🎂 Images & layers — the cake and the cache
Every Dockerfile line bakes one layer; unchanged layers are reused from cache.
🎂 image = a layer cake
FROM node:20-alpine (pre-baked)
WORKDIR /app
COPY server.js .
CMD ["node","server.js"]
1
🔁 build again,
nothing changed
every layer from cache
→ finishes in ~1 second
2
✏️ edit server.js
bottom layers: cached ✅
COPY + above: rebuilt 🔨
3
💡 the trick
order lines from
rarely-changing (bottom)
to often-changing (top)
= fast builds forever
3 📝 The Dockerfile — reading the recipe card line by line
Seven instructions cover 95% of real Dockerfiles — this repo's app uses them all.
📝 app/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY server.js .
ENV PORT=3000
EXPOSE 3000
USER node
CMD ["node","server.js"]
1️⃣ start from a pre-baked box: tiny Linux + Node
— you never install Node yourself again
2️⃣ COPY puts YOUR code inside the box
(.dockerignore decides what may enter)
3️⃣ ENV = default settings · EXPOSE = documentation
("this app listens on 3000")
4️⃣ USER: don't run as root (lesson 08) · CMD: what
happens when the box is opened — exactly ONE
process per container, in the foreground
4 🍽️ Running containers — lunch time
docker run opens the box; ports are the serving window; logs and exec are your eyes and hands.
🧊 image
hello-school:v1
🏃 container
node server.js (PID 1)
own filesystem, own network
-e APP_VERSION=v2 → settings in
1
docker run
🪟 -p 3000:3000
laptop:3000 → box:3000
2
📜 docker logs
🔧 docker exec
3
🛑 docker stop → SIGTERM, clean exit
4
5 🧊📞 Volumes & networks — the shared fridge and the intercom
Containers are disposable; volumes make data survive. Networks let containers call each other by NAME.
📞 docker network — the intercom
📦 container "web"
the app
📦 container "proxy"
calls http://web:3000
1
a NAME, not an IP — Docker DNS resolves it (k8s Services say hi 👋)
🧊 volume — the fridge
lives OUTSIDE containers,
survives docker rm 💪
2
📂 bind mount
a laptop folder mapped in —
live-edit code & config
3
6 🍽️🍽️ Docker Compose — set the whole table with one command
One YAML file describes every service; docker compose up builds and starts them all, wired together.
📄 compose.yml
web: build ./app
proxy: nginx + config
ports: 8080:80
1
🍽️ the table — one shared network
📦 web
the app · NOT exposed
reachable only by name
📦 proxy
nginx → http://web:3000
🪟 the ONLY door: :8080
3
2
compose up --build
one command up · one command down (docker compose down) · same file works on every teammate's laptop
4
7 👨🍳 Multi-stage builds — cook in the kitchen, pack only the food
Build tools live in a throwaway stage; only the result ships. Images shrink dramatically.
👨🍳 stage 1: the KITCHEN
FROM node:20-alpine AS builder
node + tools + source (~180MB)
RUN node generate.js → index.html
🗑️ thrown away after the build
1
🍱 stage 2: the LUNCHBOX
FROM nginx:alpine (~50MB)
COPY --from=builder index.html
no node, no tools, no source —
just the result. THIS ships. 🚀
3
2
only the food crosses
smaller image = faster pushes/pulls (lesson 11), fewer things to attack (lesson 08)
8 🏷️ Image hygiene — label your boxes, don't pack your keys
The four habits that separate hobby images from production images.
🏷️ 1 · real tags, never :latest in prod
hello-school:v1 / :git-sha — ":latest" is a
floating label; you can't roll back to "latest"
1
🙈 2 · .dockerignore everything private
.git, .env, node_modules, logs — COPY can't
leak what it cannot see
2
👤 3 · USER node — never run as root
a break-in to a root container is a break-in
to the machine; drop privileges in the image
3
🪶 4 · small base images (alpine/slim)
node:20 ≈ 1.1GB vs node:20-alpine ≈ 180MB —
less to pull, less to patch, less to attack
4
9 🏬 Registries — the frozen-lunchbox warehouse
A registry stores images so OTHER machines can pull them — the bridge from laptop to cloud.
🧑💻 your laptop
image built locally
🏬 registry — the warehouse
📚 repository: hello-school
shelf for ONE app's versions
🏷️ tags: v1, v2, abc123…
🔢 digest = tamper-proof fingerprint
1
docker push
☸️ EKS cluster
pulls at deploy
💻 teammate
docker pull
2
Docker Hub = the public warehouse · ECR = your company's private one (next lesson)
3
10 🏦 ECR setup — renting the bank locker
Create a private repository once; get a fresh 12-hour pass whenever you visit.
🏗️ rent the locker (once)
terraform apply (ecr/ecr.tf)
or: aws ecr create-repository
1
☁️ AWS — ECR
🔐 repository: hello-school (private)
address: ACCOUNT.dkr.ecr.REGION.amazonaws.com/hello-school
🎫 the 12-hour day pass
aws ecr get-login-password
| docker login …
2
3
now docker push/pull may enter
🔑 who may enter at all = IAM (your AWS user/role
needs ecr:* permissions — the bank checks ID first)
11 🧹 Push, pull & lifecycle — filing boxes and the janitor
The tag IS the address; push files the box, EKS pulls it, the janitor keeps the locker tidy.
🏷️ address the box
docker tag hello-school:v1 \
ACCOUNT.dkr…/hello-school:v1
1
🏦 ECR locker
📦 v3 (newest)
📦 v2
📦 v-old… expired 🧹
2
docker push
☸️ EKS deployment
image: ACCOUNT.dkr…/
hello-school:v3 → pulled
3
🧹 lifecycle policy
"keep newest 10, expire rest"
(ecr/lifecycle-policy.json)
4
12 📮 CI to cloud — the courier files the copies
In real life a robot does lessons 1–11 on every push — and hands the baton to the next two courses.
🧑💻 dev
git push
📮 CI — the courier robot
✅ test → 🍱 build →
🏷️ tag = commit SHA →
🔑 12h pass → push
(the k8s course's CircleCI does exactly this)
1
🏦 ECR
🛡️ scan on push — every image
checked for known vulnerabilities
2
☸️ next course: Kubernetes RUNS these
images across a cluster
🤖 then: ArgoCD DEPLOYS them from git,
automatically, forever
3
4