Lesson 3 - Docs, Metadata, and Deployment
Welcome to Docs, Metadata, and Deployment
Your Task Manager works, it’s configured, and it’s tested. What’s left is the gap between “runs on my laptop” and “runs on a server other people use.” That gap is smaller than it sounds, and FastAPI has already done most of the work. The interactive docs at /docs exist whether you ask for them or not — in this lesson you’ll polish them with a title, description, version, and tags so they read like a real product. Then you’ll add the two finishing touches every deployed service needs: a tiny /health endpoint so the infrastructure around your app knows it’s alive, and the actual commands and files to ship it — uvicorn in production mode and a Dockerfile that packages everything into a single image.
This is the lesson where your project stops being code and becomes a deployable service.
By the end of this lesson, you will be able to:
- Customize your API’s docs with
title,description,version, and groupedtags - Add a
/healthendpoint for load balancers, orchestrators, and uptime checks - Run your API in production with
uvicornworkers, and know when to use--reload - Containerize your app with a
Dockerfileand a pinnedrequirements.txt
Let’s make the API presentable, then ship it.
API Metadata and Tags
The first thing anyone sees when they open /docs is the title at the top of the page. By default it just says “FastAPI.” You set the real one — along with a description and version — by passing arguments when you create the app:
from fastapi import FastAPI
app = FastAPI(
title="Task Manager API",
version="1.0.0",
description="A complete, tested, deployable Task Manager API.",
)
@app.get("/health", tags=["meta"])
def health():
return {"status": "ok"}Those three arguments do real work. title and description render at the top of /docs; version shows next to the title and is how clients know which release of your API they’re talking to. All three also land in the OpenAPI schema — the machine-readable JSON that FastAPI generates at /openapi.json and that the docs page is built from. We can read them straight out of that schema with TestClient:
from fastapi.testclient import TestClient
client = TestClient(app)
info = client.get("/openapi.json").json()["info"]
print(info["title"])
print(info["version"])
print(info["description"])Task Manager API
1.0.0
A complete, tested, deployable Task Manager API.That’s the same info block any other tool reads — a generated client SDK, a documentation portal, an API gateway. Set it once on the FastAPI(...) call and every consumer of your schema gets it for free.
The other piece is tags. Notice tags=["meta"] on the /health route above. Tags group endpoints in the docs: every route tagged "tasks" appears under a tasks heading, everything tagged "auth" under auth, and so on. On a Task Manager with a dozen endpoints, that grouping is the difference between a wall of routes and a navigable page. You can optionally describe each group by passing openapi_tags to FastAPI(...):
app = FastAPI(
title="Task Manager API",
version="1.0.0",
description="A complete, tested, deployable Task Manager API.",
openapi_tags=[
{"name": "tasks", "description": "Create, read, update, and delete tasks."},
{"name": "auth", "description": "Login and token endpoints."},
{"name": "meta", "description": "Health checks and service info."},
],
)Now each section in /docs has a short blurb under its heading. You don’t have to use openapi_tags — bare tags=[...] on each route is enough to group them — but a one-line description per group makes the docs noticeably friendlier.
A /health Endpoint
When your API runs behind a load balancer, an orchestrator like Kubernetes, or an uptime monitor, something needs to periodically ask “are you still alive?” That something is a health check — a request to a known, cheap endpoint that returns a success status if the app is up. By convention it lives at /health:
@app.get("/health", tags=["meta"])
def health():
return {"status": "ok"}That’s the whole thing. No database query, no auth, no work — just a fast 200 that proves the process is running and answering requests. We can confirm it the same way we test everything else:
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.get("/health")
print(response.status_code)
print(response.json())200
{'status': 'ok'}Why does such a trivial endpoint matter so much? Because the infrastructure around your app uses it to make decisions. A load balancer polls /health every few seconds; if it stops getting 200s from one instance, it stops sending users to that instance and routes them elsewhere. An orchestrator uses the same signal to restart a crashed container. An uptime service pings it and pages you when it goes red. Keep /health fast and dependency-free — if you make it run a database query and the database hiccups, your health check fails and the platform yanks a perfectly good instance out of rotation. The point is to report whether the app is up, nothing more.
Running with uvicorn
You’ve been starting your app with uvicorn and --reload all course. --reload watches your files and restarts the server every time you save — perfect while you’re writing code, and exactly wrong in production, where it wastes resources and isn’t built for real traffic. In production you drop --reload and instead tell uvicorn where to listen and how many worker processes to run:
# Development — auto-reload on every file change
uvicorn main:app --reload# Production — listen on all interfaces, fixed port, 4 worker processes
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4Two flags carry the production version. --host 0.0.0.0 tells uvicorn to accept connections on every network interface, not just localhost — necessary so traffic from outside the machine (or container) can reach it. --port 8000 pins the port. And --workers 4 runs four independent copies of your app in parallel processes, so four requests can be handled at the same time instead of queuing behind one. A common starting point is one worker per CPU core; you tune it from there based on load. The key takeaway: --reload is a development convenience, while --host, --port, and --workers are how you actually serve users.
Containerizing with Docker
Running uvicorn by hand is fine on your laptop. To deploy reliably, you want to package your app and its exact dependencies into a single artifact that runs the same everywhere — a Docker image. Two files get you there.
First, pin your dependencies in requirements.txt so the server installs the same versions you tested with:
# requirements.txt
fastapi[standard]==0.138.1
sqlmodel==0.0.22
pydantic-settings==2.7.1
pyjwt==2.10.1
pwdlib[bcrypt]==0.2.1These are the libraries you’ve added across the course: FastAPI itself (with its standard extras, which include uvicorn and httpx), sqlmodel for the database, pydantic-settings for configuration, pyjwt for tokens, and pwdlib[bcrypt] for password hashing. Pinning versions (==) means a deploy six months from now installs exactly what works today.
Second, the Dockerfile — the recipe that builds the image:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first so this layer is cached when only your code changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY . .
# Serve the app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Read it top to bottom: start from a small official Python base image (python:3.12-slim), copy and install the requirements before the rest of the code (so Docker can reuse the cached install layer whenever only your app changes), copy your application in, then declare the command that runs when the container starts — the production uvicorn invocation from the last section. Build it with docker build -t task-manager . and run it with docker run -p 8000:8000 task-manager, and your API is serving inside a container, identical on your machine and the server.
One thing the Dockerfile deliberately leaves out: your secrets. The SECRET_KEY and database URL you set up with pydantic-settings last lesson are not baked into the image — you pass them in at run time as environment variables (docker run -e SECRET_KEY=... -e DATABASE_URL=..., or through your platform’s secrets manager). The image is the same everywhere; the configuration changes per environment.
Three deployment habits that keep you out of trouble
Use --reload only in development — it restarts on every file change, which is great while coding and wasteful (and unsupported) in production; serve real traffic with --host, --port, and --workers instead. Never bake secrets into the image — pass SECRET_KEY, database URLs, and the like as environment variables at deploy time, so the same image is safe to run anywhere. And keep /health cheap and dependency-free — it’s the signal that keeps your instance in the load balancer’s rotation; if it does real work and that work fails, the platform pulls a healthy app out of service.
Practice Exercises
Exercise 1: Read the version from the schema
You set version="1.0.0" on your FastAPI(...) call. Using TestClient, how would you assert in a test that the running app reports that version?
Hint
The version lives in the OpenAPI schema’s info block: assert client.get("/openapi.json").json()["info"]["version"] == "1.0.0". This is a real, useful test — it catches the day someone ships a release without bumping the version.
Exercise 2: Why dependency-free health?
A teammate suggests making /health query the database “so we know the DB is up too.” Why is that often a bad idea for the endpoint the load balancer polls?
Hint
If /health depends on the database and the database has a brief hiccup, the health check fails and the platform pulls your (otherwise healthy) instance out of rotation — turning a small DB blip into an outage. Keep the load-balancer check to “is the app process answering?”; if you want a deeper check, expose it as a separate endpoint that isn’t wired to traffic routing.
Exercise 3: reload vs workers
Explain in one sentence each why uvicorn main:app --reload is right for development and uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 is right for production.
Hint
--reload restarts the server whenever you save a file, giving you a fast edit-test loop while building. The production command listens on all interfaces at a fixed port and runs four worker processes so multiple requests are handled in parallel under real load — with no reload watching to waste resources.
Summary
You took the Task Manager from working code to a shippable service. Metadata — title, description, and version passed to FastAPI(...) — shows up in /docs and the /openapi.json schema, and tags (plus optional openapi_tags) group endpoints so the docs stay navigable. A /health endpoint returns a fast, dependency-free 200 that load balancers and orchestrators use to decide whether to keep sending you traffic. You serve the app with uvicorn — --reload for development, --host/--port/--workers for production — and you package it for deployment with a pinned requirements.txt and a Dockerfile built on python:3.12-slim, passing secrets in as environment variables rather than baking them into the image.
Key Concepts
- API metadata —
title,description,versiononFastAPI(...); surfaced in/docsand/openapi.json. - Tags — group endpoints in the docs;
openapi_tagsadds a description per group. - /health endpoint — a cheap, dependency-free
200for load balancers and uptime checks. - uvicorn workers —
--host/--port/--workersfor production;--reloadfor dev only. - Dockerfile + requirements.txt — package the app and pinned dependencies into one image; pass secrets as env vars at run time.
Why This Matters
Deployment is where a project becomes a product. Clean docs make your API usable by other people; a /health endpoint makes it manageable by the infrastructure it runs on; and a Dockerfile with pinned requirements makes it run the same way on a server as it does on your laptop. These are the conventions every production team relies on, and they’re the last pieces you need before the capstone, where you’ll bring testing, configuration, and deployment together into one complete, shippable Task Manager API.
Next Steps
Continue to Lesson 4 - Capstone: Build the Complete API
Bring it all together: build, test, configure, and prepare the complete Task Manager API for deployment in a single capstone project.
Back to Module Overview
Return to the Testing, Settings, Deployment, and Capstone module overview
Continue Building Your Skills
Your API now documents itself, reports its own health, and packages cleanly into a container you can run anywhere — the full journey from laptop to server. In the next lesson you’ll put every skill from this course to work at once: the capstone, where you build, test, configure, and prepare the complete Task Manager API for deployment, end to end.