Lesson 01 Code to Container
Theme:

Lab Guide — First docker build of the API

Lesson 01 · Cloud Infrastructure & DevOps · ~45 min hands-on

1. Prerequisites

2. Step 1 — Run the API locally first (the “no container” baseline)

Before adding Docker to the picture, run the URL shortener the old-fashioned way. This establishes the baseline we will reproduce exactly inside a container.

cd cloud-devops/sample-app/api
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --port 8000

In a second terminal, test the two endpoints:

curl -s localhost:8000/health
# {"status":"ok"}

curl -s -X POST localhost:8000/shorten \
  -H 'content-type: application/json' \
  -d '{"url":"https://anthropic.com"}'
# {"code":"abc123","short_url":"http://localhost:8000/abc123"}

Stop the server with Ctrl-C. This is the baseline. The container will produce the same output from the same endpoints.

3. Step 2 — Inspect the Dockerfile

The Dockerfile is already present at cloud-devops/sample-app/api/Dockerfile:

# Lesson 01: the simplest Dockerfile that boots the API.
# Lesson 02 will replace this with a multi-stage build.
FROM python:3.11-slim

WORKDIR /app

# Copy the pin file FIRST so pip-install is cached when only app.py changes.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 8000

# Use the exec form so uvicorn is PID 1 (gets SIGTERM, can shut down cleanly).
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Why each line:

4. Step 3 — Build the image

Run from the repo root (the build context is the api/ directory):

docker build -t url-api:0.1 cloud-devops/sample-app/api

Expected output: a series of [X/Y] step lines ending with something like naming to docker.io/library/url-api:0.1. First build takes ~60 seconds (Docker must pull the ~120 MB base image and run pip install). Subsequent builds with no changes take < 1 second.

Inspect what was built:

docker images url-api
# REPOSITORY   TAG   IMAGE ID       CREATED          SIZE
# url-api      0.1   abc123def456   2 minutes ago    ~180MB

Inspect the layer history:

docker history url-api:0.1
# IMAGE    CREATED       CREATED BY                                        SIZE
# ...      just now      CMD ["uvicorn", "app:app", ...]                    0B
# ...      just now      EXPOSE map[8000/tcp:{}]                            0B
# ...      just now      COPY app.py .                                      ~2kB
# ...      just now      RUN pip install --no-cache-dir ...                 ~60MB
# ...      just now      COPY requirements.txt .                            ~60B
# ...      just now      WORKDIR /app                                       0B
# ...      2 weeks ago   (python:3.11-slim base layers)                    ~120MB

5. Step 4 — Run the container (foreground)

docker run --rm -p 8000:8000 --name url-api url-api:0.1

The container blocks the terminal (uvicorn is running in the foreground). In a second terminal, run the same curl commands from Step 1 — identical output, this time served by a containerized process. Stop with Ctrl-C.

6. Step 5 — Run in the background and inspect

docker run -d -p 8000:8000 --name url-api url-api:0.1
docker ps                       # the container is listed
docker logs url-api             # uvicorn's startup line
curl -s localhost:8000/health   # {"status":"ok"}

Drop into the running container:

docker exec -it url-api /bin/sh
# Inside the container:
ps -ef                          # uvicorn is PID 1
ls /app                         # app.py + requirements.txt
exit

Stop and remove:

docker stop url-api && docker rm url-api

7. Step 6 — Prove the cache works

Open cloud-devops/sample-app/api/app.py in an editor, make a trivial no-op change (add a blank line or a comment), and save. Rebuild:

docker build -t url-api:0.2 cloud-devops/sample-app/api

Expected: Docker prints CACHED [4/5] RUN pip install --no-cache-dir .... Only the COPY app.py and final metadata layers rebuild. Build time drops to under a second.

Now edit requirements.txt (add a trailing comment line) and rebuild:

docker build -t url-api:0.3 cloud-devops/sample-app/api

Expected: the COPY requirements.txt layer and the pip install layer both rebuild (no CACHED). Changing a dependency file busts the cache for everything downstream. This is expected and correct behavior.

8. Step 7 — Two containers from one image

Show that one image can back many simultaneous containers:

docker run -d -p 8001:8000 --name url-api-1 url-api:0.1
docker run -d -p 8002:8000 --name url-api-2 url-api:0.1
docker ps                         # two instances listed
curl -s localhost:8001/health     # both respond
curl -s localhost:8002/health
docker stop url-api-1 url-api-2 && docker rm url-api-1 url-api-2

Both containers share the read-only image layers; only the thin writable top layer is per-container. This is the foundation of horizontal scaling.

9. What you should leave with

10. Troubleshooting