Skip to content

Commit d570e83

Browse files
travisjneumanclaude
andcommitted
feat: add Module 09 — Docker & Deployment curriculum
Five projects covering Dockerfile basics, multi-stage builds, docker-compose with PostgreSQL, GitHub Actions CI pipelines, and production configuration (env vars, health checks, non-root users, structured logging). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09e1b07 commit d570e83

38 files changed

Lines changed: 2554 additions & 0 deletions
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# ============================================================================
2+
# Dockerfile — First Dockerfile
3+
# ============================================================================
4+
# A Dockerfile is a recipe for building a container image. Each instruction
5+
# adds a layer to the image. Docker caches layers, so unchanged layers are
6+
# reused on subsequent builds (this makes rebuilds fast).
7+
#
8+
# Build this image:
9+
# docker build -t first-dockerfile .
10+
#
11+
# Run a container from the image:
12+
# docker run -p 8000:8000 first-dockerfile
13+
# ============================================================================
14+
15+
# --------------------------------------------------------------------------
16+
# FROM — choose the base image.
17+
# Every Docker image starts from a parent image. python:3.12-slim is an
18+
# official Python image based on Debian with unnecessary packages removed.
19+
# "slim" means it is smaller than the full image (~150 MB vs ~900 MB).
20+
#
21+
# Why not python:3.12 (full)? — Too large. Includes compilers and tools
22+
# you do not need at runtime.
23+
# Why not python:3.12-alpine? — Alpine uses musl libc instead of glibc.
24+
# Some Python packages (numpy, pandas) do
25+
# not have pre-built wheels for Alpine and
26+
# require compiling from source, which is
27+
# slow and error-prone.
28+
# --------------------------------------------------------------------------
29+
FROM python:3.12-slim
30+
31+
# --------------------------------------------------------------------------
32+
# WORKDIR — set the working directory inside the container.
33+
# All subsequent commands (COPY, RUN, CMD) execute relative to this path.
34+
# If the directory does not exist, Docker creates it.
35+
# This is like running "cd /app" at the start of a shell session.
36+
# --------------------------------------------------------------------------
37+
WORKDIR /app
38+
39+
# --------------------------------------------------------------------------
40+
# COPY requirements.txt first (before copying the rest of the code).
41+
# Docker caches each layer. If requirements.txt has not changed, Docker
42+
# reuses the cached pip install layer and skips the slow download step.
43+
# This is called "layer caching" and it makes rebuilds much faster.
44+
# --------------------------------------------------------------------------
45+
COPY requirements.txt .
46+
47+
# --------------------------------------------------------------------------
48+
# RUN — execute a command during the build.
49+
# pip install reads requirements.txt and installs the listed packages.
50+
#
51+
# --no-cache-dir — do not store pip's download cache inside the image.
52+
# The cache wastes space because you will never pip install
53+
# again inside this image.
54+
# --------------------------------------------------------------------------
55+
RUN pip install --no-cache-dir -r requirements.txt
56+
57+
# --------------------------------------------------------------------------
58+
# COPY . . — copy the rest of your application code into the image.
59+
# The first "." is the source (your project directory on the host).
60+
# The second "." is the destination (/app inside the container, because
61+
# WORKDIR is set to /app).
62+
#
63+
# This step is separate from COPY requirements.txt so that code changes
64+
# do not invalidate the pip install cache layer.
65+
# --------------------------------------------------------------------------
66+
COPY . .
67+
68+
# --------------------------------------------------------------------------
69+
# EXPOSE — document which port the container listens on.
70+
# This does NOT actually publish the port. It is metadata for anyone
71+
# reading the Dockerfile. The actual port mapping happens at runtime
72+
# with "docker run -p 8000:8000".
73+
# --------------------------------------------------------------------------
74+
EXPOSE 8000
75+
76+
# --------------------------------------------------------------------------
77+
# CMD — the default command to run when the container starts.
78+
# This starts uvicorn, which serves the FastAPI application.
79+
#
80+
# --host 0.0.0.0 — listen on all network interfaces inside the container.
81+
# Without this, the server only listens on 127.0.0.1
82+
# (localhost inside the container), which is unreachable
83+
# from the host machine.
84+
# --port 8000 — match the EXPOSE declaration above.
85+
#
86+
# CMD uses the "exec form" (JSON array) instead of the "shell form"
87+
# (plain string). The exec form runs uvicorn as PID 1, which means it
88+
# receives shutdown signals (SIGTERM) directly. The shell form wraps the
89+
# command in /bin/sh, which can swallow signals and prevent graceful
90+
# shutdown.
91+
# --------------------------------------------------------------------------
92+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Module 09 / Project 01 — First Dockerfile
2+
3+
Home: [README](../../../../README.md)
4+
5+
## Focus
6+
7+
Writing a Dockerfile, building an image, and running a container.
8+
9+
## Why this project exists
10+
11+
Docker packages your application and all its dependencies into a single image that runs the same way on every machine. No more "it works on my laptop" problems. This project teaches you the fundamental Docker workflow: write a Dockerfile, build an image, run a container. Every deployment pipeline in the industry starts here.
12+
13+
## Run
14+
15+
### Without Docker (verify the app works locally first)
16+
17+
```bash
18+
cd projects/modules/09-docker-deployment/01-first-dockerfile
19+
python app.py
20+
```
21+
22+
Visit http://127.0.0.1:8000 to confirm it works, then stop the server with `Ctrl+C`.
23+
24+
### With Docker
25+
26+
**Step 1 — Build the image.** This reads the Dockerfile and creates an image named `first-dockerfile`:
27+
28+
```bash
29+
docker build -t first-dockerfile .
30+
```
31+
32+
You will see Docker execute each instruction. The first build downloads the base image and installs dependencies (slow). Subsequent builds reuse cached layers (fast).
33+
34+
**Step 2 — Run a container from the image:**
35+
36+
```bash
37+
docker run -p 8000:8000 first-dockerfile
38+
```
39+
40+
The `-p 8000:8000` flag maps port 8000 on your machine to port 8000 inside the container.
41+
42+
**Step 3 — Visit your app:**
43+
44+
- http://127.0.0.1:8000 — root endpoint
45+
- http://127.0.0.1:8000/health — health check
46+
- http://127.0.0.1:8000/docs — interactive API docs
47+
48+
**Step 4 — Stop the container** with `Ctrl+C`.
49+
50+
### Useful Docker commands
51+
52+
```bash
53+
docker images # list all images on your machine
54+
docker ps # list running containers
55+
docker ps -a # list all containers (including stopped)
56+
docker rm <container_id> # remove a stopped container
57+
docker rmi first-dockerfile # remove the image
58+
```
59+
60+
## Expected output
61+
62+
Visiting http://127.0.0.1:8000 returns:
63+
64+
```json
65+
{"message": "Hello from Docker!", "version": "1.0.0"}
66+
```
67+
68+
Visiting http://127.0.0.1:8000/health returns:
69+
70+
```json
71+
{"status": "healthy"}
72+
```
73+
74+
## Alter it
75+
76+
1. Add a `GET /info` endpoint that returns `{"container": True, "python_version": "3.12"}`. Rebuild the image and verify.
77+
2. Change the `CMD` in the Dockerfile to run on port 9000. Update the `EXPOSE` instruction and the `docker run` command to match.
78+
3. Add a `--name my-app` flag to `docker run` so you can refer to the container by name instead of ID.
79+
80+
## Break it
81+
82+
1. Remove the `COPY requirements.txt .` and `RUN pip install` lines from the Dockerfile. Rebuild and try to run. What error do you get?
83+
2. Change `--host 0.0.0.0` to `--host 127.0.0.1` in the CMD instruction. Rebuild and run. Can you reach the app from your browser? Why not?
84+
3. Swap the order of `COPY requirements.txt .` and `COPY . .` so all files are copied in one step. Does it still work? What happens to build caching when you change only `app.py`?
85+
86+
## Fix it
87+
88+
1. Restore the `COPY requirements.txt` and `RUN pip install` lines. The app cannot start without its dependencies installed in the image.
89+
2. Change the host back to `0.0.0.0`. Inside a container, `127.0.0.1` only accepts connections from inside the container itself, not from the host machine.
90+
3. Separate the two COPY steps again. With a single COPY, every code change invalidates the pip install cache and forces a slow reinstall.
91+
92+
## Explain it
93+
94+
1. What is the difference between a Docker image and a Docker container?
95+
2. Why does the Dockerfile copy `requirements.txt` and run `pip install` before copying the rest of the code?
96+
3. What does `-p 8000:8000` mean in the `docker run` command? What happens if you use `-p 3000:8000`?
97+
4. Why does the CMD use `--host 0.0.0.0` instead of `--host 127.0.0.1`?
98+
99+
## Mastery check
100+
101+
You can move on when you can:
102+
103+
- write a Dockerfile from scratch without looking at this one,
104+
- build an image and run a container with port mapping,
105+
- explain why layer order matters for build caching,
106+
- describe the difference between EXPOSE and `-p`.
107+
108+
## Next
109+
110+
Continue to [02-multi-stage-build](../02-multi-stage-build/).
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ============================================================================
2+
# Project 01 — First Dockerfile
3+
# ============================================================================
4+
# A minimal FastAPI application designed to run inside a Docker container.
5+
# This is the same kind of app you built in Module 04, but now you will
6+
# package it into a container image so it runs identically on any machine.
7+
#
8+
# Run locally (without Docker):
9+
# python app.py
10+
#
11+
# Run with Docker (after building the image):
12+
# docker run -p 8000:8000 first-dockerfile
13+
#
14+
# Then visit:
15+
# http://127.0.0.1:8000 — root endpoint
16+
# http://127.0.0.1:8000/health — health check
17+
# http://127.0.0.1:8000/docs — interactive API documentation
18+
# ============================================================================
19+
20+
from fastapi import FastAPI
21+
22+
# ----------------------------------------------------------------------------
23+
# Create the FastAPI application instance.
24+
# The title and version appear in the auto-generated /docs page.
25+
# ----------------------------------------------------------------------------
26+
app = FastAPI(
27+
title="First Dockerfile App",
28+
version="1.0.0",
29+
)
30+
31+
32+
# ----------------------------------------------------------------------------
33+
# Route 1: GET /
34+
# A simple root endpoint that confirms the app is running.
35+
# When this response comes from a Docker container, you know the entire
36+
# build-and-run pipeline is working.
37+
# ----------------------------------------------------------------------------
38+
@app.get("/")
39+
def read_root():
40+
"""Return a welcome message confirming the app is running."""
41+
return {"message": "Hello from Docker!", "version": "1.0.0"}
42+
43+
44+
# ----------------------------------------------------------------------------
45+
# Route 2: GET /health
46+
# Health check endpoints are essential in containerized environments.
47+
# Orchestrators like Docker Swarm and Kubernetes ping this endpoint to
48+
# decide whether the container is healthy. If it stops responding, the
49+
# orchestrator restarts the container automatically.
50+
# ----------------------------------------------------------------------------
51+
@app.get("/health")
52+
def health_check():
53+
"""Health check endpoint for container orchestrators."""
54+
return {"status": "healthy"}
55+
56+
57+
# ----------------------------------------------------------------------------
58+
# Run the server when executed directly (local development without Docker).
59+
#
60+
# Inside a Docker container, you run uvicorn via the CMD instruction in the
61+
# Dockerfile instead of using this block. The host is set to "0.0.0.0" so
62+
# the server accepts connections from outside the container. Using
63+
# "127.0.0.1" inside a container would make the app unreachable because
64+
# the container has its own network namespace.
65+
# ----------------------------------------------------------------------------
66+
if __name__ == "__main__":
67+
import uvicorn
68+
69+
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Notes — First Dockerfile
2+
3+
## What I learned
4+
5+
6+
## What confused me
7+
8+
9+
## What I want to explore next
10+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Dependencies for the First Dockerfile project.
2+
# This file is copied into the Docker image and used by pip install.
3+
# Keeping it separate from the module-level requirements.txt means the
4+
# Docker image only installs what this specific app needs.
5+
fastapi>=0.109
6+
uvicorn[standard]>=0.27
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# ============================================================================
2+
# .dockerignore — Files to exclude from the Docker build context
3+
# ============================================================================
4+
# When you run "docker build", Docker sends your entire project directory
5+
# (the "build context") to the Docker daemon. The .dockerignore file tells
6+
# Docker which files to skip. This:
7+
#
8+
# 1. Reduces build context size (faster builds).
9+
# 2. Prevents sensitive or unnecessary files from ending up in the image.
10+
# 3. Avoids invalidating Docker layer caches with irrelevant file changes.
11+
# ============================================================================
12+
13+
# --------------------------------------------------------------------------
14+
# Python bytecode — generated at runtime, not needed in the image.
15+
# __pycache__ directories contain .pyc files that Python creates when it
16+
# imports a module. They are platform-specific and should be regenerated
17+
# inside the container.
18+
# --------------------------------------------------------------------------
19+
__pycache__/
20+
*.pyc
21+
*.pyo
22+
23+
# --------------------------------------------------------------------------
24+
# Virtual environment — the Docker image builds its own.
25+
# Including a local .venv would waste hundreds of megabytes and the packages
26+
# might not be compatible with the container's operating system.
27+
# --------------------------------------------------------------------------
28+
.venv/
29+
venv/
30+
env/
31+
32+
# --------------------------------------------------------------------------
33+
# Git history — not needed at runtime and can be very large.
34+
# --------------------------------------------------------------------------
35+
.git/
36+
.gitignore
37+
38+
# --------------------------------------------------------------------------
39+
# IDE and editor files — configuration specific to your machine.
40+
# --------------------------------------------------------------------------
41+
.vscode/
42+
.idea/
43+
*.swp
44+
*.swo
45+
46+
# --------------------------------------------------------------------------
47+
# Data and log files — often large and environment-specific.
48+
# --------------------------------------------------------------------------
49+
data/
50+
*.log
51+
52+
# --------------------------------------------------------------------------
53+
# Docker-related files — no need to copy Dockerfiles into the image.
54+
# --------------------------------------------------------------------------
55+
Dockerfile
56+
Dockerfile.*
57+
docker-compose*.yml
58+
.dockerignore
59+
60+
# --------------------------------------------------------------------------
61+
# Documentation — not needed in the runtime image.
62+
# --------------------------------------------------------------------------
63+
README.md
64+
notes.md
65+
*.md

0 commit comments

Comments
 (0)