Hello!
If you’ve ever thought “I’d love to spin up a little web app, but I don’t want to
fight with servers, dependencies, and ‘works on my machine’ gremlins” — this one’s
for you. We’re going to build a tiny web API with FastAPI, wrap it in Docker,
and have it running in a clean, throwaway container in about fifteen minutes.
No magic, no 400-line boilerplate. Just enough to see the whole loop: write code →
build image → run container → hit it in a browser. Once you’ve got that muscle memory, everything bigger is just more of the same.
Let’s go.
Why FastAPI + Docker?
FastAPI is a modern Python web framework that’s stupid-fast to write and gives you
a few things for free that used to be a pain:
- Automatic request validation (you describe your data, it enforces it)
- Auto-generated interactive API docs — you’ll see this in a minute and it’ll sell you
- Async support baked in
Docker solves the other half of the problem: packaging. Instead of “install Python
3.12, then these exact library versions, then set these env vars,” you ship one image
that runs the same on your laptop, your buddy’s laptop, and a server in a data center.
Together they’re a fantastic combo for going from idea to running-thing quickly.
What you’ll need
- Docker Desktop installed and running (get it here)
- A text editor
- That’s it. You don’t even need Python installed locally — Docker brings its own.
Step 1: Project layout
Make a folder and create three files. That’s the whole project:
gigs-api/
├── main.py # our FastAPI app
├── requirements.txt # Python dependencies
└── Dockerfile # how to build the container image
Step 2: The app (main.py)
We’ll build a dead-simple “gig tracker” — a little API to list and add band gigs.
(I play in a rock band called 8ball, so this is what my sample apps always end up
being about. Use whatever you like — tasks, bookmarks, coffee orders.)
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
app = FastAPI(title="Gigs API")
# A tiny in-memory "database". Restarts when the app restarts — perfect for a demo,
# swap it for a real DB later.
gigs = [
{"id": 1, "venue": "The Rialto", "city": "Tucson", "date": "2026-08-15"},
]
# Describe what a new gig looks like. FastAPI validates incoming data against this.
class Gig(BaseModel):
venue: str
city: str
date: str
@app.get("/", response_class=HTMLResponse)
def home():
return """
<h1>🎸 Gigs API</h1>
<p>It's alive! Try these:</p>
<ul>
<li><a href="/gigs">/gigs</a> — list all gigs (JSON)</li>
<li><a href="/docs">/docs</a> — interactive API docs</li>
<li><a href="/health">/health</a> — health check</li>
</ul>
"""
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/gigs")
def list_gigs():
return gigs
@app.post("/gigs", status_code=201)
def add_gig(gig: Gig):
new_gig = {"id": len(gigs) + 1, **gig.dict()}
gigs.append(new_gig)
return new_gig
Four endpoints: an HTML landing page, a health check (handy for later — Docker and
load balancers love these), and GET/POST for gigs. Notice we never wrote code to
parse JSON or return errors for bad input — the Gig model handles that for us.
Step 3: Dependencies (requirements.txt)
fastapi
uvicorn[standard]
uvicorn is the server that actually runs FastAPI. The [standard] bit pulls in some
nice-to-haves like auto-reload.
Step 4: The Dockerfile
This is the recipe Docker follows to build your image:
FROM python:3.12-slim
WORKDIR /app
# Install deps first — Docker caches this layer, so rebuilds are fast
# unless requirements.txt changes.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Now copy the app code.
COPY main.py .
EXPOSE 8000
# Start the server, listening on all interfaces so it's reachable from outside the container.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
One thing worth calling out: we copy requirements.txt and install before copying
the app code. That ordering means Docker only re-runs the slow pip install when your
dependencies change — not every time you tweak a line of code. Little trick, big time
savings.
Step 5: Build and run
From inside the gigs-api/ folder:
# Build the image and tag it "gigs-api"
docker build -t gigs-api .
# Run it, mapping your machine's port 8000 to the container's port 8000
docker run -p 8000:8000 gigs-api
Open http://localhost:8000 and you should see the landing page. 🎉
Now the fun part — go to http://localhost:8000/docs. FastAPI generated a full
interactive API explorer for you, for free. You can add a gig right there in the browser:
expand POST /gigs, click Try it out, edit the JSON, and hit Execute. Then
refresh /gigs and watch your new gig show up.
Want to do it from the command line instead?
curl -X POST http://localhost:8000/gigs \
-H "Content-Type: application/json" \
-d '{"venue": "Club Congress", "city": "Tucson", "date": "2026-09-02"}'
To stop the container, hit Ctrl+C in the terminal where it’s running.
Bonus: Docker Compose for a nicer dev loop
Typing that docker run line over and over gets old. Drop a docker-compose.yaml
next to your Dockerfile:
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app # live-mount your code
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
Now:
docker compose up
That --reload plus the volume mount means the server restarts automatically every
time you save a file. Edit main.py, hit save, refresh the browser — your change is
live. Run docker compose down when you’re done.
Where to go from here
You just built and containerized a real web API. That same pattern scales up to
basically anything:
- Add a database — swap the in-memory list for SQLite or Postgres (Compose makes
adding a database container easy). - Add HTML templates — use Jinja2 to render real pages instead of that inline string.
- Ship it — that image runs anywhere Docker runs: a VPS, a cloud service, a Raspberry Pi.
The whole point of this exercise is the loop — write, build, run, poke at it. Once
that feels natural, the framework and the container stop being obstacles and start being
tools you barely think about.
Now go build something. And if it ends up being about your band too, well… you’re in
good company. 🤘
— netwookie