Docker Lesson Plan
A progressive curriculum to master Docker through hands-on practice.
Lesson 1: Images and Layers
Section titled “Lesson 1: Images and Layers”Goal: Understand how Docker images work and how layers affect builds.
Concepts
Section titled “Concepts”A Docker image is a read-only template built from layers. Each instruction in a Dockerfile creates a layer. Layers are cached — unchanged layers skip rebuilding.
Key terms:
- Image — a snapshot of a filesystem plus metadata (entrypoint, env vars)
- Layer — a diff on top of the previous layer, produced by one Dockerfile instruction
- Tag — a human-readable label pointing to a specific image (e.g.,
nginx:1.25-alpine) - Digest — an immutable SHA256 hash identifying an exact image
┌─────────────────────────┐│ CMD ["nginx", "-g"] │ ← metadata layer├─────────────────────────┤│ COPY index.html │ ← your code├─────────────────────────┤│ RUN apt-get install │ ← dependencies├─────────────────────────┤│ FROM ubuntu:22.04 │ ← base image└─────────────────────────┘Exercises
Section titled “Exercises”-
Pull and list images
Terminal window docker pull nginx:alpinedocker pull python:3.12-slimdocker images # List local images -
Inspect layers
Terminal window docker history nginx:alpine # Show layer stackdocker inspect nginx:alpine # Full metadata as JSONdocker inspect --format '{{.Config.Cmd}}' nginx:alpine -
Compare image sizes
Terminal window docker pull node:22 # Full imagedocker pull node:22-alpine # Alpine variantdocker images node # Compare sizes -
Clean up
Terminal window docker rmi node:22 # Remove specific imagedocker image prune # Remove dangling images
Checkpoint
Section titled “Checkpoint”Run docker history on two images. Explain why Alpine images are smaller (musl
libc, no extras) and why layer count matters for cache hits.
Lesson 2: Running Containers
Section titled “Lesson 2: Running Containers”Goal: Run, manage, and inspect containers through their full lifecycle.
Concepts
Section titled “Concepts”A container is a running instance of an image. It adds a writable layer on top of the image’s read-only layers. When the container stops, the writable layer persists until the container is removed.
Container lifecycle: create → start → running → stop → removed
Exercises
Section titled “Exercises”-
Run your first container
Terminal window docker run hello-world # Pull + run + exitdocker run -d --name web nginx # Detached, nameddocker ps # Running containersdocker ps -a # Include stopped -
Interact with a running container
Terminal window docker exec -it web bash # Shell into containerls /usr/share/nginx/html # Browse the filesystemexitdocker logs web # View stdout/stderrdocker logs -f web # Follow (like tail -f)docker top web # Processes insidedocker stats web # Live CPU/memory -
Port mapping and environment variables
Terminal window docker run -d --name web2 \-p 8080:80 \-e NGINX_HOST=localhost \nginxcurl http://localhost:8080 # Hit the containerdocker port web2 # Show port mappings -
Stop, restart, remove
Terminal window docker stop web # Graceful (SIGTERM)docker start web # Restart stopped containerdocker kill web2 # Force (SIGKILL)docker rm web web2 # Remove containersdocker run --rm nginx echo "hi" # Auto-remove on exit
Checkpoint
Section titled “Checkpoint”Run an nginx container with port 8080 mapped to port 80. Curl it. Shell in with
exec. Stop and remove it. docker ps -a shows nothing.
Lesson 3: Writing Dockerfiles
Section titled “Lesson 3: Writing Dockerfiles”Goal: Write efficient Dockerfiles with proper layer ordering and security.
Concepts
Section titled “Concepts”A Dockerfile is a recipe for building an image. Each instruction creates a layer. Order matters for caching: put things that change rarely at the top, things that change often at the bottom.
Key instructions:
| Instruction | Purpose |
|---|---|
FROM | Base image |
WORKDIR | Set working directory |
COPY | Copy files from host |
RUN | Execute command during build |
ENV | Set environment variable |
EXPOSE | Document exposed port |
CMD | Default command (overridable) |
ENTRYPOINT | Fixed command |
USER | Set user for subsequent commands |
ARG | Build-time variable |
HEALTHCHECK | Container health check |
Exercises
Section titled “Exercises”-
Create a simple app and Dockerfile
Terminal window mkdir docker-lesson && cd docker-lessonecho '<h1>Hello Docker</h1>' > index.html# DockerfileFROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmlEXPOSE 80Terminal window docker build -t my-site .docker run -d -p 8080:80 --name site my-sitecurl http://localhost:8080docker rm -f site -
Optimize layer caching
FROM node:22-alpineWORKDIR /app# Dependencies first (changes rarely)COPY package*.json ./RUN npm ci --only=production# Code last (changes often)COPY . .EXPOSE 3000CMD ["node", "server.js"]Change a source file and rebuild. The
npm cilayer caches. -
Add a .dockerignore
.dockerignore .gitnode_modules*.md.envDockerfileTerminal window docker build -t my-app .# Build context is smaller — faster builds -
Run as non-root
FROM node:22-alpineWORKDIR /appCOPY --chown=node:node . .RUN npm ci --only=productionUSER nodeCMD ["node", "server.js"]
Checkpoint
Section titled “Checkpoint”Build an image. Change only a source file and rebuild — the dependency layer
must cache (look for CACHED in build output). Verify the container runs as a
non-root user with docker exec <container> whoami.
Lesson 4: Multi-Stage Builds
Section titled “Lesson 4: Multi-Stage Builds”Goal: Separate build and runtime environments to reduce image size.
Concepts
Section titled “Concepts”Multi-stage builds use multiple FROM instructions. Each FROM starts a new
stage. You copy artifacts from earlier stages into the final image, leaving
build tools behind.
┌──────────────────────┐ ┌──────────────────────┐│ Build Stage │ │ Runtime Stage ││ ──────────────────── │ │ ──────────────────── ││ Full SDK/compiler │ │ Minimal base image ││ Source code │ ──→│ Compiled binary only ││ Dev dependencies │ │ No build tools ││ ~800MB+ │ │ ~50MB │└──────────────────────┘ └──────────────────────┘Exercises
Section titled “Exercises”-
Build a Go binary with multi-stage
Terminal window mkdir multi-stage && cd multi-stagemain.go package mainimport ("fmt""net/http")func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello from multi-stage build")})http.ListenAndServe(":8080", nil)}Terminal window go mod init multi-stage# Build stageFROM golang:1.23 AS builderWORKDIR /appCOPY go.mod main.go ./RUN CGO_ENABLED=0 go build -o server .# Runtime stageFROM alpine:3.20COPY --from=builder /app/server /serverEXPOSE 8080CMD ["/server"]Terminal window docker build -t multi-stage .docker images multi-stage # Check the size -
Compare with single-stage
Dockerfile.single FROM golang:1.23WORKDIR /appCOPY go.mod main.go ./RUN go build -o server .EXPOSE 8080CMD ["./server"]Terminal window docker build -t single-stage -f Dockerfile.single .docker images | grep stage # Compare sizes -
Multi-stage for Node.js
FROM node:22 AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run buildFROM node:22-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesUSER nodeEXPOSE 3000CMD ["node", "dist/server.js"] -
Build a specific stage
Terminal window docker build --target builder -t my-app-builder .# Useful for CI: run tests in the build stage
Checkpoint
Section titled “Checkpoint”Build both single-stage and multi-stage images for the Go app. The multi-stage image should be 10x+ smaller. Explain why: the final image contains only the binary, not the Go toolchain.
Lesson 5: Volumes and Persistence
Section titled “Lesson 5: Volumes and Persistence”Goal: Persist data across container restarts using volumes and bind mounts.
Concepts
Section titled “Concepts”Containers are ephemeral — data in the writable layer disappears when the container is removed. Volumes solve this.
| Type | What It Does | Use Case |
|---|---|---|
| Named volume | Docker-managed directory on the host | Database storage, caches |
| Bind mount | Maps a specific host path into the container | Development (live reload) |
| tmpfs | In-memory filesystem, never written to disk | Secrets, scratch space |
Exercises
Section titled “Exercises”-
Named volumes
Terminal window docker volume create mydatadocker volume lsdocker volume inspect mydata# Write datadocker run --rm -v mydata:/data alpine sh -c 'echo "persisted" > /data/test.txt'# Read from a new containerdocker run --rm -v mydata:/data alpine cat /data/test.txt# Output: persisted -
Bind mounts for development
Terminal window mkdir -p ~/docker-dev && echo '<h1>Live reload</h1>' > ~/docker-dev/index.htmldocker run -d --name dev \-v ~/docker-dev:/usr/share/nginx/html:ro \-p 8080:80 nginxcurl http://localhost:8080# Edit ~/docker-dev/index.html — changes appear immediatelyecho '<h1>Updated</h1>' > ~/docker-dev/index.htmlcurl http://localhost:8080docker rm -f dev -
Database with persistent volume
Terminal window docker run -d --name pg \-e POSTGRES_PASSWORD=secret \-v pgdata:/var/lib/postgresql/data \postgres:16-alpine# Create datadocker exec -it pg psql -U postgres -c "CREATE TABLE test (id int);"docker exec -it pg psql -U postgres -c "INSERT INTO test VALUES (1);"# Destroy and recreate containerdocker rm -f pgdocker run -d --name pg2 \-e POSTGRES_PASSWORD=secret \-v pgdata:/var/lib/postgresql/data \postgres:16-alpine# Data survivesdocker exec -it pg2 psql -U postgres -c "SELECT * FROM test;"docker rm -f pg2 -
Cleanup
Terminal window docker volume rm mydata pgdatadocker volume prune # Remove all unused volumes
Checkpoint
Section titled “Checkpoint”Create a named volume. Write a file from one container, read it from another. Remove both containers and verify the data survives until the volume itself is removed.
Lesson 6: Networking
Section titled “Lesson 6: Networking”Goal: Connect containers to each other and to the host.
Concepts
Section titled “Concepts”Docker creates isolated networks. Containers on the same network can reach each other by name. Containers on different networks cannot communicate without explicit configuration.
| Driver | Purpose |
|---|---|
bridge | Default; isolated network on one host |
host | Share the host’s network (no isolation) |
none | No networking |
Exercises
Section titled “Exercises”-
Default bridge network
Terminal window # Containers on the default bridge can't resolve each other by namedocker run -d --name web nginxdocker run --rm alpine ping -c 2 web# Fails: "bad address 'web'"docker rm -f web -
Custom bridge network
Terminal window docker network create mynetdocker network lsdocker run -d --name web --network mynet nginxdocker run --rm --network mynet alpine ping -c 2 web# Succeeds: DNS resolution works on custom networksdocker rm -f web -
Multi-container communication
Terminal window docker network create app-net# Start a backend APIdocker run -d --name api --network app-net \-e PORT=3000 nginx# Start a frontend that calls the APIdocker run --rm --network app-net alpine \wget -qO- http://api:80# Output: nginx default page — proves name resolution worksdocker rm -f apidocker network rm app-net -
Inspect and debug networks
Terminal window docker network create debug-netdocker run -d --name debug-box --network debug-net alpine sleep 3600docker network inspect debug-net # Show connected containersdocker exec debug-box cat /etc/hostsdocker exec debug-box nslookup debug-boxdocker rm -f debug-boxdocker network rm debug-net
Checkpoint
Section titled “Checkpoint”Create a custom network. Run two containers on it. Prove they can reach each
other by name with ping or wget. Remove both and verify
docker network inspect shows no connected containers.
Lesson 7: Docker Compose
Section titled “Lesson 7: Docker Compose”Goal: Define and run multi-service applications with Compose.
Concepts
Section titled “Concepts”Docker Compose describes a multi-container application in a single YAML file.
One command (docker compose up) creates networks, volumes, and containers for
the entire stack.
Compose handles:
- Service definitions (images, builds, ports, environment)
- Dependencies (
depends_on) - Shared networks (created automatically)
- Named volumes
- Development overrides
Exercises
Section titled “Exercises”-
Create a Compose file
Terminal window mkdir compose-demo && cd compose-democompose.yaml services:web:image: nginx:alpineports:- "8080:80"volumes:- ./html:/usr/share/nginx/html:rodepends_on:- apiapi:image: python:3.12-alpinecommand: python -m http.server 5000expose:- "5000"Terminal window mkdir html && echo '<h1>Compose works</h1>' > html/index.htmldocker compose up -dcurl http://localhost:8080docker compose psdocker compose logsdocker compose down -
Add a database
compose.yaml services:web:image: nginx:alpineports:- "8080:80"db:image: postgres:16-alpineenvironment:POSTGRES_USER: appPOSTGRES_PASSWORD: secretPOSTGRES_DB: mydbvolumes:- pgdata:/var/lib/postgresql/dataredis:image: redis:7-alpinevolumes:pgdata:Terminal window docker compose up -ddocker compose exec db psql -U app -d mydb -c "SELECT 1;"docker compose downdocker compose down -v # Also remove volumes -
Build from Dockerfile
compose.yaml services:app:build: .ports:- "3000:3000"volumes:- .:/app- /app/node_modulesenvironment:- NODE_ENV=developmentTerminal window docker compose up --build # Build and startdocker compose build # Build only -
Useful Compose commands
Terminal window docker compose up -d # Start detacheddocker compose ps # List servicesdocker compose logs -f web # Follow one servicedocker compose exec web sh # Shell into servicedocker compose restart api # Restart one servicedocker compose stop # Stop (keep containers)docker compose down # Stop and removedocker compose config # Validate YAML
Checkpoint
Section titled “Checkpoint”Write a compose.yaml with nginx and postgres. Start both with
docker compose up -d. Connect to postgres with docker compose exec. Tear
down with docker compose down -v. No containers, no volumes remain.
Lesson 8: Debugging and Security
Section titled “Lesson 8: Debugging and Security”Goal: Troubleshoot container failures and harden images for production.
Concepts
Section titled “Concepts”Debugging containers requires different tools than debugging host processes. You
cannot SSH into a container — use exec, logs, and inspect instead.
Security starts at the Dockerfile. The attack surface of your container is the sum of everything in the image.
Exercises
Section titled “Exercises”-
Debug a container that won’t start
Terminal window # Create a broken imageecho 'FROM alpine' > Dockerfile.brokenecho 'CMD ["nonexistent-command"]' >> Dockerfile.brokendocker build -t broken -f Dockerfile.broken .docker run --name fail broken# Container exits immediatelydocker logs fail # See the errordocker inspect fail --format '{{.State.ExitCode}}'# Enter the image to investigatedocker run -it --entrypoint sh brokenwhich nonexistent-command # Not foundexitdocker rm fail -
Debug a running container
Terminal window docker run -d --name debug-web nginxdocker exec -it debug-web bashcat /etc/nginx/nginx.conf # Check configcurl localhost # Test from insideexitdocker stats debug-web # CPU and memorydocker inspect debug-web --format '{{.NetworkSettings.IPAddress}}'docker diff debug-web # Filesystem changes since startdocker rm -f debug-web -
Copy files for analysis
Terminal window docker run -d --name inspect-me nginxdocker cp inspect-me:/etc/nginx/nginx.conf ./nginx.confdocker cp ./custom.conf inspect-me:/etc/nginx/conf.d/docker rm -f inspect-me -
Apply security best practices
# Secure DockerfileFROM node:22-alpine# Non-root userRUN addgroup -g 1001 -S app && \adduser -u 1001 -S app -G appWORKDIR /app# Dependencies (owned by root, not writable by app)COPY package*.json ./RUN npm ci --only=production && npm cache clean --force# Source (owned by app user)COPY --chown=app:app . .USER appHEALTHCHECK --interval=30s --timeout=5s --retries=3 \CMD wget -qO- http://localhost:3000/health || exit 1EXPOSE 3000CMD ["node", "server.js"]Security checklist:
Practice Why Use Alpine or distroless Fewer packages = smaller attack surface Run as non-root Limits blast radius if compromised Pin image versions Reproducible builds, no surprise changes Add HEALTHCHECK Orchestrators detect unhealthy containers Scan images docker scout cvesor Trivy catches CVEsNo secrets in image Use runtime injection or BuildKit secrets -
Scan an image
Terminal window docker scout cves nginx:alpine # Built-in scanner# Or with Trivy:# trivy image nginx:alpine -
Set resource limits
Terminal window docker run -d --name limited \--memory 256m \--cpus 0.5 \nginxdocker stats limited # Verify limitsdocker rm -f limited
Checkpoint
Section titled “Checkpoint”Build the secure Dockerfile. Verify: non-root user (whoami), health check
passes (docker inspect --format '{{.State.Health.Status}}'), image is under
200MB (docker images).
Practice Projects
Section titled “Practice Projects”Project 1: Containerize a Python App
Section titled “Project 1: Containerize a Python App”Write a Flask or FastAPI app. Create a Dockerfile with:
- Multi-stage build (install deps in build stage, copy to runtime)
- Non-root user
- Health check endpoint
.dockerignorethat excludes.git,__pycache__,.venv
Target: image under 100MB.
Project 2: Multi-Service Compose Stack
Section titled “Project 2: Multi-Service Compose Stack”Create a compose.yaml with:
- Web server (nginx) reverse-proxying to an API
- API service (Python or Node.js) connecting to a database
- PostgreSQL with a named volume
- Redis for caching
- Custom network separating frontend from backend
Verify: docker compose up -d starts everything; the web server reaches the
API; data survives docker compose down and docker compose up -d.
Project 3: Debug a Broken Container
Section titled “Project 3: Debug a Broken Container”Given this deliberately broken Dockerfile, find and fix all issues:
FROM ubuntu:latestRUN apt-get install -y curl python3COPY . .ENV DB_PASSWORD=hunter2CMD python3 app.pyIssues to find: missing apt-get update, latest tag, no .dockerignore, no
WORKDIR, secret in ENV, running as root, shell form CMD.
Command Reference
Section titled “Command Reference”| Stage | Must Know |
|---|---|
| Beginner | docker run docker ps docker stop docker rm docker images |
| Daily | docker build docker exec docker logs docker compose up/down |
| Power | docker inspect docker stats docker network docker volume |
| Advanced | docker scout docker buildx docker save/load multi-stage |
See Also
Section titled “See Also”- Docker — Quick reference: images, containers, Compose, networking
- CLI Pipelines — Pipe composition used alongside Docker commands
- Operating Systems Lesson Plan — Processes, namespaces, and cgroups that Docker builds on
- Security Lesson Plan — Threat modeling and hardening beyond container scope