Free lab: run the app, then see where it goes
TL;DR
- You will: see the finished system this package builds, then run its starting app on your own machine. You'll use its API, take its database away, break its code on purpose, and open the messy documents it will learn to answer from.
- Time: about 45 minutes.
- Cost: $0. No Azure account needed. Everything runs on your machine.
- You need: Git, Docker (Docker Desktop on macOS and Windows), and a terminal.
- New words: container: a packaged copy of a program that runs the same everywhere. Docker Compose: starts several containers together with one command. endpoint: one address on a web server that does one job. readiness check: an endpoint that says whether the server can do real work. RAG: find the right passages first, then let a model answer from them. OCR: software that reads text from a picture of a page.
Where this goes
RAG on Azure has 11 labs. You start with a small notes app called Fieldnotes. You finish with a question-answering system built into it. The three outputs below come from that finished system, captured on live Azure.
A question answered from a scanned paper memo (lab 7):
$ python3 scripts/ask.py "When do the old office badges stop working, and which vendor is doing the replacement?"
ANSWER: Old badges stop working on April 14, 2026, and the replacement vendor is
NorthPeak Cloud field services.[1]
CITATIONS:
[1] scanned-facilities-memo.pdf
[2] policy-badge-access.html
[3] vendors.csv
(2.33s)
The first citation is a scan: a picture of a page, with no text inside. Lab 4 teaches the app to read it. You'll open that same file in step 7.
A question the documents can't answer (lab 7):
$ python3 scripts/ask.py "What is our policy on quantum computing purchases?"
ANSWER: I can't find that in your documents.
It says so instead of making something up. A test checks those exact words.
A quality gate for every change (lab 9):
$ pytest -m eval
tests/eval/test_eval_gate.py ... [100%]
3 passed, 38 deselected in 190.78s
Break the search on purpose and this gate fails, naming what broke. A team runs it before any change ships.
This free lab is the starting line. You'll run the app those answers come from, before any AI is in it. The app has three parts:
browser ──→ frontend (React, :5173) ──→ backend (FastAPI, :8000) ──→ postgres (:5432)
serves the UI owns all logic & AI owns all data
Concepts
Why Docker Compose runs the app
Fieldnotes is three programs that have to run together: a database, a backend, and a frontend. Without help, you'd install PostgreSQL, Python, and Node, then start each one in its own terminal, in the right order, every time.
Docker packages each program, with everything it needs, into a container that runs the same way on any machine. Docker Compose starts all three containers together, in the right order, from one file and one command. It works like a restaurant's kitchen manager. You say "open the kitchen", and every station gets staffed in the right order. Here, docker compose up is "open the kitchen".
Alive is not the same as ready
A server can be running and still be unable to do anything useful. It might be up while its database is down. So the backend has two small status endpoints. /healthz answers "is the process running?" /readyz is the readiness check: "can it do real work right now?" It tests the database connection to find out.
Think of a shop. The lights being on doesn't mean the shop is open. The sign flipped to OPEN, with staff at the till, does. Production systems use the difference: traffic only goes to copies of the app that are ready. You'll see both answers split apart in step 5.
Words versus meaning
Fieldnotes ships with keyword search. It finds notes that contain the words you typed. That works until people describe a problem in their own words. Search "checkout broke" and you get nothing, even though there's a postmortem about exactly that.
Search by meaning fixes this, and it's the first half of RAG. RAG means the app first finds the passages in your documents that answer a question. Then a language model writes the answer from those passages only, with references. It's an open-book exam: the skill is finding the right page fast. You'll see the gap yourself in step 3.
Hands-on
Step 1 — Check your tools
Run
git --version
docker --version
docker compose version
Expected result (your versions may differ):
git version 2.34.1
Docker version 29.4.0, build 9d7ad9f
Docker Compose version v5.1.2
If it fails: command not found, or an error about the Docker daemon, means Docker isn't installed or isn't running. Install or start Docker Desktop, then run the commands again.
Step 2 — Get the code and start the app
Run
git clone https://github.com/labwrights/free-labs
cd free-labs/rag-on-azure/app
docker compose up
⏱ The first run takes 3–6 minutes while images download and build. Later runs take seconds.
Expected result: the log stream settles, and near the end you see:
backend-1 | {"ts":"...","level":"INFO","logger":"fieldnotes","msg":"startup complete"}
frontend-1 | ➜ Local: http://localhost:5173/
Leave this terminal running. Open a second terminal in the same app folder for the next steps.
If it fails: port is already allocated means another program already uses port 5173, 8000, or 5432. Stop that program, or change the left-hand number of that ports: line in docker-compose.yml.
✅ CHECKPOINT 1 — in the second terminal:
curl localhost:8000/healthz && curl localhost:8000/readyzYou should see
{"status":"ok"}{"status":"ready"}. The whole stack is up.
Step 3 — Use the app like a person
Open http://localhost:5173. You should see eight starter notes, including a deploy runbook, a postmortem, and an expense policy.
- Open "Postmortem: checkout outage 2026-07-18" and read it.
- Click New note, write anything, give it a tag, and save.
- Search
postmortemin the top bar. The list narrows to one note. - Now search
checkout broke. You get nothing, though the postmortem is about exactly that. That gap is what this package fixes. - Open any note and click Summarize. It answers "Not built yet — this feature lights up when you reach its lab." Try Auto-tag, Search docs, and Ask your docs too.
The screens ship finished, and the backend grows lab by lab. Each button starts working in the lab that builds what it calls:
| Button | Starts working in |
|---|---|
| Summarize | lab 2, your first call to a language model |
| Auto-tag | lab 3, structured output |
| Search docs | labs 5 and 6, search by meaning on Azure AI Search |
| Ask your docs | lab 7, cited answers from your documents |
Step 4 — Talk to the API directly
The browser is only one client. Everything it does goes through the backend's API, and you can call that API yourself.
Run
curl "localhost:8000/api/notes?q=checkout%20broke"
Expected result:
[]
That's step 3's empty search, without the browser. Now create a note from the terminal:
curl -X POST localhost:8000/api/notes \
-H "Content-Type: application/json" \
-d '{"title": "My first note", "body_md": "Created from the terminal.", "tags": ["test"]}'
Expected result (your id and times will differ):
{"id":9,"title":"My first note","body_md":"Created from the terminal.","tags":["test"],"created_at":"2026-10-26T22:01:56.342663Z","updated_at":"2026-10-26T22:01:56.342666Z"}
Refresh the browser, and the note is there. The backend also checks every request before it touches the database. Send an empty title, -d '{"title": ""}', and you get HTTP 422 with "String should have at least 1 character".
Prefer clicking to typing? Open http://localhost:8000/docs. FastAPI builds this page from the code itself. It lists every endpoint, each with a Try it out button.
Step 5 — Take the database away
Run
docker compose stop db
curl localhost:8000/healthz
curl localhost:8000/readyz
Expected result:
Container app-db-1 Stopped
{"status":"ok"}
{"status":"not ready","reason":"database unreachable"}
What just happened: the process is still alive, so healthz says ok. It can't do real work, so readyz says not ready, with HTTP status 503. A load balancer seeing that 503 stops sending traffic to this copy. Bring the database back:
docker compose start db
curl localhost:8000/readyz
You should see {"status":"ready"}. If it still says not ready, wait a few seconds and run the curl again.
Step 6 — Run the tests, then break the code
Run
docker compose exec backend pytest
Expected result:
....... [100%]
7 passed in 0.18s
Now break something on purpose. Open backend/src/fieldnotes/api/health.py in any editor. Find the line return {"status": "ok"}, change ok to okay, and save.
Two things happen. First, the running app changed: curl localhost:8000/healthz now answers {"status":"okay"}. Compose mounts your code folder into the container, and the server reloads on every save. Second, run the tests again:
docker compose exec backend pytest
Expected result (the end of the output):
E AssertionError: assert {'status': 'okay'} == {'status': 'ok'}
...
FAILED tests/test_health.py::test_healthz - AssertionError: assert {'status':...
1 failed, 6 passed in 0.21s
A one-word change, caught before it shipped, with the exact difference shown. Put the file back and check:
git checkout -- backend/src/fieldnotes/api/health.py
docker compose exec backend pytest
You should see 7 passed again. In the full package you add tests for everything you build, and keep them passing before and after every change.
Step 7 — Open the messy documents
The full package answers questions from 100 workplace documents in six formats. Five of them are in the free repo.
Run
ls ../sample-docs
Expected result:
legacy-scan-archive.pdf policy-badge-access.html runbook-checkout-api.md scanned-facilities-memo.pdf service-catalog.csv
Open each one in your editor, browser, or PDF viewer:
runbook-checkout-api.md: clean, structured markdown. The easy case.service-catalog.csv: a table. It's structured, but it isn't prose. Turning rows into searchable text is a lab 4 problem.policy-badge-access.html: a web page. The app has to strip the tags and keep the words.scanned-facilities-memo.pdf: find the date the old badges stop working. Now try to select a word. You can't, because the page is a picture. This is the memo the finished system cited at the top of this page. Lab 4 reads it with OCR.legacy-scan-archive.pdf: try to open it. It's broken, on purpose. Lab 4's pipeline must report it as failed, clearly, instead of crashing or quietly skipping it.
What just happened: you've seen the three kinds of input every real pile of documents has: clean text, structured data that isn't prose, and garbage. A system that has only seen clean markdown fails on its first scanned PDF.
Bring it down
docker compose down # stop everything; your notes survive
docker compose down -v # also wipe the data, so the next start is fresh
Run-rate right now: $0/day. In the full package, every lab that uses Azure ends with a teardown back to $0/day.
Check yourself
1. Your monitoring shows healthz green but readyz red. What's wrong, and what breaks first?
The process is running but can't reach its database. Anything that touches data fails, so the API is up but useless. A load balancer that checks readiness stops sending traffic to this copy, which is what you want.
2. Why does "checkout broke" find nothing when a checkout-outage postmortem exists?
Keyword search matches letters, not meaning. Neither "checkout broke" nor any form of it appears in the note. Search by meaning, built in labs 5 and 6, finds it.
3. Your edit to health.py showed up without a restart. Why?
Compose mounts your backend/src folder into the container, and the server watches it. Every save reloads the code, so you see each change in seconds.
Interview corner
Each paid lab ends with questions like these, and answers you can give because you built the thing.
- "Liveness versus readiness: why have both?" They answer different questions: "should I restart this?" and "should I send it traffic?" One check doing both jobs causes needless restarts, or traffic sent to broken copies.
- "Walk me through what happens when you run
docker compose up." Compose reads the YAML file and builds or downloads the images. It creates a private network and starts the services in order, waiting on health checks. Then it publishes ports and mounts folders.
What the full package builds
You've run the starting app. The 11 labs build the system from the top of this page into it, one working piece at a time:
| Lab | You build |
|---|---|
| 1 | The full tour: the app, its code, and all 100 documents |
| 2 | A keyless Azure OpenAI client and the Summarize feature |
| 3 | Streaming answers and structured-output auto-tagging |
| 4 | Ingestion for six formats, OCR for scans, and visible failure |
| 5 | Chunking, embeddings, and search by meaning |
| 6 | Azure AI Search with hybrid keyword and vector search |
| 7 | Cited, streamed answers, with a refusal you can test |
| 8 | A retrieval-quality harness, with ranker and chunk-size experiments |
| 9 | An evaluation gate for CI, with a model as judge |
| 10 | Traces, and the token and dollar cost of every answer |
| 11 | Injection defense in layers, a token budget, and a cache |
Every lab works the way this one did: concepts in plain English, then exact commands with real expected output. You also get reference solutions for every lab, The AI Engineer's Handbook (65 topics), a capstone with an autograder, an interview pack, and flashcards.