AI Dev Tools Zoomcamp FAQ
Table of Contents
General Course-Related Questions
# How do I access the course modules and materials?
All course materials are in the GitHub repo. Each module has its own folder (e.g. 01-overview, 03-mcp), and cohort-specific homework is under the cohorts/ folder.
For the 2026 cohort, the week-by-week folders are:
- Week 1: 01-ai-native-workflow
- Week 2: 02-development
- Week 3: 03-deployment
- Week 4: 04-devops
Start from the folder for your current week — it gathers everything for that week in one place.
Lectures are pre-recorded and available in the YouTube playlist. New workshops or updated videos are announced on Slack and Telegram. If you don't see an announcement, assume everything you need is already there.
Each homework has a strict deadline listed on the schedule; after the deadline the form closes. Submissions appear on the leaderboard. You can earn extra points by sharing your learning publicly with the hashtag #aidevtools and tagging Alexey Grigorev or DataTalksClub.
# Are the sessions live or recorded?
Lectures are pre-recorded and available in the YouTube playlist. New workshops or updated videos are announced on Slack and Telegram.
Live sessions are primarily for Q&A and are announced separately - there is no regular schedule for them. When a live session takes place, the recording is posted on YouTube.
# Where do I submit the homework?
Instructions and the submission form are on GitHub. You can update your submission until the deadline.
# How are homework assignments scored?
Homework grade = points for the questions + 1 point for the FAQ + up to 7 points for learning in public.
- Learning in public: up to 7 points, depending on how many platforms you shared your post on (e.g. LinkedIn, X, blog) and the quality of the post.
- FAQ: 1 point. To get it, contribute to the FAQ repo and add the link to your PR in your homework submission.
Optional questions are scored too - read the submission form carefully. The homework is for practice and does not affect your certificate. If you think you weren't graded, log in to check your results or search for your name on the leaderboard.
# How do I create a GitHub repository to submit my homework?
You'll create a repo on GitHub, connect your local folder to it, push your code, and submit the repo link. You need Git installed and a GitHub account.
Create a folder (e.g.
ai-dev-tools-zoomcamp) and put your homework files inside.On github.com, create a new public repo (skip README/license for now) and click "Create repository".
In a terminal, move into your folder and run:
git init git remote add origin https://github.com/USERNAME/ai-dev-tools-zoomcamp.git git remote -vAdd and commit your files:
git add . git commit -m "Add homework for module 1"Push and set tracking:
git push -u origin mainFuture updates are just
git add ./git commit -m "..."/git push.Open your repo page on GitHub, copy the URL, and paste it into the homework submission form.
# What are the prerequisites and system requirements for this course?
You don't need any previous experience with AI tools. A basic ability to program (Python, JavaScript, or similar) is enough to follow the materials and complete the projects.
# Am I too late to join the course?
No. You're only late for submitting the first couple of homeworks, and they aren't required for the certificate. To earn the certificate you need to pass a project and evaluate 3 of your peers' projects, and that part starts later in the course.
# Which free AI tools can I use for the course?
You can complete the course on free or low-cost tools:
- Module 1: free tools like Groq or Gemini.
- Module 2: Cursor or GitHub Copilot (pro free trial possible).
- Module 4: a small API deposit (~$5) for OpenAI/Anthropic.
- Other free alternatives are shared on Slack (e.g. Qoder, Kilo Code, Antigravity).
Besides the tools listed above, free models (free API) exist, although sometimes temporarily, which could still be suitable for the duration of a course:
- OpenRouter free models
- Kilo free models
- Nous Portal models (free models listed on top)
For example, at time of writing, Hy3 and Minimax M3 are free, albeit rate-limited.
# Does this course cover AI engineering topics like LangChain, vector databases, or RAG?
No. This course is about using AI dev tools to be a more productive developer, not about building LLM applications. LangChain/LangGraph, vector databases, retrieval, and reranking are AI engineering topics - those are covered in the free LLM Zoomcamp. The only overlap is Module 4, where you build a coding agent to understand how these tools work under the hood.
# If AI writes the code, do I still need to understand it, and will I lose my coding skills?
Yes, you still need to understand the code. With AI tools your job shifts from writing code to reading, reviewing, and improving it - and you're the one who will maintain it, so you need to follow what's happening. The way to keep your skills sharp is to stick to core engineering practices: write tests, commit to git regularly, keep documentation, and ask the assistant to explain code you don't follow. Vibe coding (just accepting whatever works) is fine up to a point, but it breaks down once the codebase grows or something stops working.
# Can I take the course on a locked-down work laptop without admin rights?
Yes. Use GitHub Codespaces - you only need a modern browser (or VS Code) and an internet connection, and you can install whatever you need inside the Codespace. GitHub Copilot and OpenAI Codex both work in Codespaces. The free tier is usually generous enough for the course.
# Will the course teach debugging AI-generated code or prompt engineering to avoid hallucinations?
There's no dedicated module for either, but you'll pick them up in practice. AI-generated code isn't guaranteed to work, so you learn to debug it as you go - and the course's engineering practices (writing tests, committing to git) surface broken or hallucinated code for you. For example, when a model invents a method that doesn't exist, a failing test makes it fix it. You can also ask the assistant to debug a specific error, or paste in the correct docs when it uses an outdated API. Deep prompt engineering is more of an LLM Zoomcamp topic.
# What's the minimum project score to pass and get the certificate?
The pass mark is set for each cohort. For the 2026 cohort, the draft project criteria have 19 criteria worth 41 points, and you need 24 points to pass (see the project page). The criteria are still a draft and may change, so check the project page and the "Project outcomes" section of the course dashboard for your cohort.
Passing the project isn't enough on its own. You also need to evaluate three of your peers' projects. A project submitted without the peer reviews doesn't pass, whatever its score.
# Is a free plan (for example, free Claude) enough for the course, or do I need a paid subscription?
Most likely the free plan won't be enough. Plan to spend at least $10 on a paid plan from a provider of your choice.
A $10 plan, such as OpenCode Go, should be fine. Be careful in Module 1: long
/goal runs over a whole website can drain the quota. Test the workflow on a
small scope until you understand the concept, then use it as usual in the later
modules.
# Is my homework peer reviewed?
No, peer reviews are only for projects. Homework is scored on the course management platform.
# Can I check my homework answers after the deadline? Is there a reference solution?
You can still do the homework after the deadline to test yourself, but you can't submit it once the form is closed.
There's no reference solution, but after the homework is scored, the course management platform shows the correct answers, so compare yours with them.
# I registered with two accounts and submitted homework from both. Can you merge them?
No, the accounts can't be merged. Choose one of them and use it for the rest of the course. It doesn't affect your chances of getting a certificate.
Unit 1: AI-Native Developer Workflow
# What do I need to install for the course?
Not much, and no GPU or cloud compute. You need an editor (VS Code, or Cursor which is VS Code-based), Python (the course uses uv, which can also install Python for you on Windows), and Node.js (install it via nvm for the JavaScript parts). You do not need Android Studio or any mobile tooling unless your own project targets mobile. Installing the tools for your operating system is up to you - the course shows how to use them, not how to install them.
# Can I use Codex, Gemini CLI, or another assistant instead of Cursor or GitHub Copilot?
Yes. The course works with many coding assistants, so use what you're comfortable with. Codex has similar functionality to Copilot and Cursor, and people use it alongside them. Gemini CLI is a good free alternative - it works fine for the Django homework in Unit 1, and you can use it in Unit 2 instead of a paid Cursor/Copilot subscription. Antigravity is another free option.
# Which assistant mode should I use - agent, edit, or ask?
For building things, use agent mode - it can plan and make changes across multiple files. Use edit mode when you only want to change a single file. Ask mode (just chatting without editing) is rarely needed for coding. Inline autocomplete is also handy, though sometimes noisy.
# Is there an IDE with unlimited access to coding agents, and how do I deal with quota limits?
There's no truly unlimited option - free tiers (Antigravity, Gemini CLI) hit quota limits and you may have to wait hours or switch models. To manage it:
- Track usage (
/statsin CLIs, quota-tracker extensions in Antigravity). - Use Cursor's "auto mode" so you don't burn a specific model's limit.
- Toggle between tools (Cursor, Antigravity, Claude Code).
- Pay as you go with a small budget for Claude or GPT, or use Codex if you have a ChatGPT subscription.
- At work, ask for a Cursor or Claude Code license.
# Should I use pip or uv?
Use uv by default. When you specifically need pip, use uv pip.
# What's a good Plan vs Act workflow, and should I switch models for planning vs coding?
You don't have to over-engineer it. With a capable model, it can plan and execute in one go. Tools like Cursor let you explicitly plan first, review the plan, then execute - but that's often redundant for everyday edits. For models, use a stronger one for big tasks and a faster/auto mode for follow-ups (Cursor's "auto mode" also avoids burning a specific model's usage limit). Switching models mid-task can cause context loss, so try to keep one model per task.
# What is AGENTS.md and how do I use it for regular commits and prompt history?
AGENTS.md gives coding agents durable, project-specific context and instructions, so you don't repeat the same instructions in every prompt. Put the file at the repo root (see agents.md) and document stable project information there:
- Project structure and conventions
- Commands for running the application and tests
- Coding standards and constraints
- Important architectural decisions
- Testing and validation requirements
For example, a line like "commit code to git regularly" makes the agent commit on its own. Most assistants read AGENTS.md automatically (Antigravity is a notable exception). Cursor also has Commands for repetitive instructions.
Keep long-lasting rules in AGENTS.md; task-specific requirements belong in the spec or backlog rather than in AGENTS.md.
To keep prompt history, save the agent's plan files (e.g. PLAN.md / todo.md) or export your chat - handy when you switch models or revert code. See the Unit 1 lesson for how specs, backlogs, and AGENTS.md fit into the AI-native workflow.
# How do I define rules in Antigravity?
Rules in Antigravity are Markdown files with standards, conventions, and constraints that guide the agent. You can manage them through the UI or by creating the files directly — see the official Rules docs.
To use the UI: open the Customizations panel via the "…" dropdown at the top of the agent panel, go to the Rules panel, and click + Global or + Workspace.
Workspace rules
Workspace rules live in the .agents/rules folder of your workspace or git root. Create a .md file there (e.g. coding-standards.md) with your constraints in plain Markdown. (The older .agent/rules path still works, but .agents/rules is the current default.)
Global rules
Global rules apply across all workspaces and live in ~/.gemini/GEMINI.md.
At the rule level you can set how a rule activates: Manual (via @ mention in the agent input), Always On, Model Decision (the model decides from your description), or Glob (applies to files matching a pattern like src/**/*.ts).
# I can't sign in with Google on Antigravity. What can I do?
If authentication keeps failing - even in Chrome and after clearing the cache - the most reliable workaround is to create a temporary new Google account just for Antigravity. This often works when nothing else does.
# How do I fix Claude Code login failing with "localhost refused to connect" in GitHub Codespaces?
Claude Code's OAuth login opens a browser tab pointing at http://localhost:PORT/callback. In GitHub Codespaces, that callback URL can fail to load because the browser reaches it through Codespaces' port-forwarding proxy instead of talking directly to the listener.
Workaround:
- Run
claudeand let it open the login link in your browser. - When the page fails to load, copy the full callback URL from the address bar (for example
http://localhost:35251/oauth/callback?code=...). - In a second terminal tab inside the same Codespace, run:
curl "http://localhost:35251/oauth/callback?code=..."
- Your original
claudeterminal should log in immediately.
Why this works: the curl runs inside the Codespace on the same machine as the listener, so it bypasses the forwarding proxy.
If this still doesn't resolve it, check Anthropic's Troubleshoot installation and login docs for other known causes.
# How do people record their screen for the demo videos?
On macOS, use the built-in recorder with cmd+shift+5. On Windows 11, use win+shift+S. Loom is also popular for quick, shareable recordings.
# How do I create an empty GitHub repository, clone it, add files, commit, and push?
Do this step-by-step from your terminal:
Create a local folder (or use your existing project folder).
Create an empty repository on GitHub:
- Go to GitHub → New repository.
- Choose public/private.
- Create it without adding a README (so it stays “empty”).
Clone the empty repo to your machine:
- Copy the repo URL from GitHub (HTTPS).
- Run:
git clone https://github.com/USERNAME/REPO.git cd REPO(If your code is already in another folder, you can instead
git initthere, but cloning is the simplest for a truly empty repo.)Copy (or move) your files into the cloned folder (the
REPO/directory).Stage files:
git add .Commit:
git commit -m "Add project files"Push to GitHub (replace
mainif your branch name differs):git push -u origin main
If you hit an authentication prompt on push, use the credentials method recommended by GitHub (now typically a GitHub token instead of a password).
# How do I fix ModuleNotFoundError when running pytest on a src-layout project on Linux?
When tests live in tests/ but import a package under src/ (e.g. from weekly_feedback.cli import main), plain pytest fails with ModuleNotFoundError because pytest adds the test file's directory to sys.path, not the project root — so the src/ package is invisible.
Point pytest at src — the directory containing the importable package, not the repo root. In pyproject.toml (as in the Unit 1 weekly-feedback project):
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
Or in a pytest.ini at the repo root:
[pytest]
testpaths = tests
pythonpath = src
The pythonpath option needs pytest 7+. Alternatively, pip install -e . makes the package importable everywhere without pytest config.
# Codex CLI asks for approval every time it runs a GitHub CLI command. How do I stop approving the same command repeatedly?
When Codex CLI prompts for approval, it’s because the command pattern matches a rule that requires user confirmation.
If you trust the command, choose the prompt option like “Yes, and don’t ask again for commands that start with …”. Codex will save an execution rule for that command prefix, so future matching
gh ...commands run without asking again.For example, instead of approving
gh issue view 1 --json number,title,body,state,labels,url,commentsevery time, select “Yes, and don’t ask again” and Codex will allow that matchinggh issue view ...prefix for future runs.If you want Codex to manage approval prompting more interactively, start it with
codex --ask-for-approval on-requestso approval is controlled on demand rather than being fully disabled.
Note: "trusted project"/sandbox trust and "ask-for-approval" are separate—trusting a project doesn't automatically disable all command-approval prompts. Avoid using codex --ask-for-approval never unless you understand the security implications.
See the Codex CLI reference (--ask-for-approval, --sandbox) and the sandbox and approvals guide.
# When two groomed backlog issues depend on each other (cycle), should I implement them one at a time anyway?
Yes, implement in dependency order—but fix the backlog first and don’t work around a cycle.
Why it happens: the grooming/PM agent typically looks at one issue at a time, so Issue A’s acceptance criteria can assume an endpoint that Issue B will introduce, while Issue B’s constraints assume A already exists. With the “linked follow-up” rule, both issues can end up assuming the other, creating a grooming bug.
What to do:
- Identify which issue “owns” the shared piece (usually the model or the endpoint).
- Move that shared work into the owning issue, delete it from the other issue, and use a one-way dependency link (e.g., “blocked by #…”) instead of mutual/blocking links.
- If they can’t be separated because they’re truly one vertical slice, merge them into a single issue and close the other as a duplicate.
- After grooming (before implementing anything), run a quick dependency check: ask the assistant to list dependencies between all open issues and flag any cycles. Catching cycles early prevents engineers from writing code against a spec that can’t hold.
Then implement in dependency order once the cycle is removed. "One issue at a time" is about keeping each unit of work small enough to review, not about the issue count.
# How can I speed up the PM / Engineer / QA orchestrator loop?
The orchestrated loop is slow by design. Every issue goes through grooming, implementation and QA as separate subagent runs, and the lesson notes that it "takes significantly more time and tokens than a direct loop with only a software engineer." To speed it up:
- Skip the full loop for small or simple projects. A single engineer agent, or a plain prompt, is often enough.
- Keep issues small but not tiny. Group related work so each issue is one reviewable slice. Dozens of micro-issues multiply the PM and QA overhead.
- Before you start the loop, ask the assistant to review the backlog for dependency cycles and unclear acceptance criteria. Those are what cause FAIL -> reimplement churn.
- Run the subagents on a faster or cheaper model, and keep the stronger model for the orchestrator or PM if your tool supports per-agent models.
- Do a dry run on one issue and watch what each subagent does before you run
/goal work through the backlog.
# How do I set up Kiro with my personal AWS account to use my existing AWS credits?
Subscribe to Kiro through the Kiro console in your AWS account instead of buying an individual Kiro plan. The subscription is then billed to that AWS account, where eligible credits can cover it.
The setup steps change, so follow the official Kiro docs:
- Subscribe your team: set up IAM Identity Center, create the Kiro profile, and assign a subscription tier to your user.
- Connect IAM Identity Center: requirements for the Identity Center instance.
- Enterprise billing: tiers and how charges are billed.
If you already have a paid individual Kiro account, cancel it and sign out of Kiro. Then sign back in with your IAM Identity Center user. Check Billing and Cost Management > Credits to confirm that your credits apply to Kiro charges.
# Why did my GitHub issue close automatically when I merged a PR, even though the PR description said "do not close #123"?
GitHub closes a linked issue after you merge a pull request into the default branch. The PR description or a commit message on that branch must contain a closing keyword followed by the issue number. A PR title doesn't close an issue.
For example, Closes #123 closes issue #123. The text Don't close #123 still contains close #123, so GitHub treats it as a closing reference. See GitHub's documentation on linking a pull request to an issue.
To link an issue without closing it, write Refs #123 or Related to #123 in the PR description.
Unit 1 Homework
# `makemigrations` fails with “impossible to add the field ... auto_now_add=True” — how do I fix it?
This error happens when you add created_at = models.DateTimeField(auto_now_add=True) to a Django model that already has rows in the database. Django can't invent timestamps for the existing rows, so it stops and asks for a default.
It's easy to miss because the prompt is interactive. If your coding agent runs makemigrations non-interactively, it crashes with EOFError: EOF when reading a line instead of showing the question — which looks like an unrelated bug.
Three common fixes:
Answer the interactive prompt Run
uv run python manage.py makemigrationsyourself and choose the option that sets the default (often Django suggeststimezone.now).Recreate the database (if it's disposable) For early homework/dev scenarios where the DB can be thrown away (the sqlite file should be in
.gitignoreanyway):
rm -f db.sqlite3
rm -f chores/migrations/0*.py # only if those migrations are also throwaway
uv run python manage.py makemigrations
uv run python manage.py migrate
- Avoid
auto_now_addand use a default instead If you want a timestamp without hitting the prompt, use a default value:
from django.utils import timezone
created_at = models.DateTimeField(default=timezone.now)
Note: auto_now_add=True sets the value only on insert and is effectively read-only on updates. If you need to override timestamps in a seed script or tests, prefer default=timezone.now.
Tip for AI agents: if your agent evolves the models multiple times in one session, run makemigrations after each meaningful model change—small migrations are easier to interpret and revert.
# Do I need prior Django or Python knowledge?
No. Homework 1 is beginner-friendly, and the AI assistants guide you through each step.
# Which Python version is compatible with Django 5.2.9?
Django 5.2.9 needs Python 3.10 or above; on older versions (3.9 or less) the server won't start.
Check your system Python version (macOS):
/usr/bin/python3 --version 2>&1
To fix it, create and use a virtual environment with Python 3.10+:
/usr/local/bin/python3.10 -m venv .venv --clear
source .venv/bin/activate
Then restart the Django server.
# Do I have to use Django for Homework 1 if I want to build with a different stack?
Yes, use Django for Homework 1. The multiple-choice questions are based on the Django project, so the answers won't match with another stack.
You can still get value from your own stack. Get the homework answers with Django, and AI can help with that. Then build what you want with the tools you prefer.
Unit 2: Build and Ship an AI-Assisted Full-Stack App
# I get CORS errors locally after building the frontend. What's going on?
This is the "CORS trap" with a monolith setup: if your backend disables CORS whenever it detects a frontend/dist folder (assuming production = same origin), a local build will fool it into blocking your localhost dev server. Don't rely on folder checks - use an explicit environment variable (e.g. APP_ENV) to decide prod vs dev, and configure the frontend to use relative paths (/api) so the production build respects the same-origin policy.
# Where can I find the prompts Alexey used in the videos?
They aren't documented in the repo. You can extract them yourself - paste a screenshot into ChatGPT, or run the video transcripts through a tool like NotebookLM to pull out the instructions. If you compile a list, a PR to the course repo is very welcome.
# Why does `http://localhost:8000` return `{ "detail": "Not Found" }` after starting my FastAPI backend?
When you start a FastAPI server, FastAPI will only respond to routes you explicitly define. The {"detail":"Not Found"} response means your app has no endpoint registered for the root path (/), so FastAPI returns a 404.
To confirm the server is up, use the built-in docs or an existing endpoint:
http://localhost:8000/docshttp://localhost:8000/<your_defined_route>
If you want http://localhost:8000/ to return something, add a root route:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def health_check():
return {"message": "API is running"}
Common cause: you’re hitting / but your main routes are under a prefix (e.g. /v1/...), or you haven’t added any @app.get("/") handler yet.
Unit 2 Homework
# Playwright installed Chromium, but my coding agent still can't open a page. Why, and what should I do?
npx playwright install chromium only downloads the browser binary into a user cache. Chromium also needs OS-level shared libraries (NSS, GBM, fonts, audio). Those are installed by npx playwright install --with-deps chromium (or npx playwright install-deps), which calls the system package manager and needs root. In a sandbox without sudo that step can't run, so the binary is on disk but fails to launch. The launch error lists the exact missing libraries.
What to do:
If you have root (
id -uprints0, orsudo -n truesucceeds), run:npx playwright install --with-deps chromiumIf you control the container, use Playwright's official Docker image, which already includes the browsers and their dependencies.
Otherwise, check the connection yourself: start the backend and the frontend, open the app in your browser, and watch the Network tab. The homework allows manual verification.
As a fallback, the agent can check the API contract and CORS with
curl, sending theOriginheader your browser would send (adjust ports and routes):curl -i -X OPTIONS http://localhost:8000/api/items \ -H "Origin: http://localhost:5173" \ -H "Access-Control-Request-Method: POST"The response must contain
access-control-allow-originmatching your frontend origin. This is weaker than a browser test, so make sure the agent reports that no real browser check happened.
# npm install crashes with "TypeError: Cannot read properties of null (reading 'edgesOut')". How do I fix it?
This is a crash inside npm's dependency resolver (Arborist), not a bug in your code. It's usually triggered by an inconsistent node_modules or package-lock.json, for example in a prototype generated by another tool, or after an interrupted install.
Try these in order:
Reinstall from scratch:
rm -rf node_modules package-lock.json npm installIn PowerShell:
Remove-Item -Recurse -Force node_modules, package-lock.json.Upgrade npm and retry:
npm install -g npm@latestIf it only fails when you add test tooling, pin the vitest major version that the reference app uses (quote the range so your shell doesn't interpret
^):npm install -D "vitest@^4.1.0" jsdom @testing-library/react @testing-library/dom
Once it installs, commit package-lock.json. Anyone who clones your repo can then run npm ci, which installs exactly the locked tree.
Unit 3: Test, Containerize, and Deploy
# For deployment, should I use a monolith (one container) or split the frontend and backend?
Both work. A monolith (bundling the frontend build inside the backend container) is simpler to deploy, but on a free tier like Render the whole app sleeps. A split setup (static site + web service) keeps the frontend fast while the backend wakes up, but runs two instances and uses more hours. For the course either is fine - pick the monolith for simplicity unless free-tier cold starts bother you.
# Railway can't read the port from railway.toml. How do I fix it?
Instead of relying on railway.toml, ask an AI assistant to create an entrypoint script that runs the app with uvicorn (rather than fastapi run). Example entrypoint:
exec uv run uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
# How do I deploy my Dockerized API to Render (free tier)?
Quickest path:
- Push your image to Docker Hub:
docker push <your-username>/<image>:latest - In Render, create a new Web Service → "Deploy an existing image from a registry" → paste the Docker Hub URL.
- Set the start command (e.g.
uvicorn app:app --host 0.0.0.0 --port $PORT). Render injects$PORTat runtime — listen on it, don't hardcode. - Free tier sleeps after ~15 min idle, so the first request after that takes ~30s to wake up.
Render is a popular free-tier choice for project deployment because it doesn't require a credit card. Other options: Fly.io, Google Cloud Run (both require a card), Railway, PythonAnywhere.
Don't post Docker Hub itself as your "deployment" — it's a registry, not a runtime.
# My AI-generated frontend (Lovable, v0, Bolt) uses SSR and the build has no static `index.html` for FastAPI to serve. How do I fix it?
If your AI builder scaffolds your frontend with SSR (e.g., Next.js/Remix/TanStack Start or an SSR-enabled setup), the build often won’t emit a static HTML shell like index.html. That breaks the course’s single-container deployment approach where FastAPI serves the frontend’s static files.
Fix it by switching the frontend build to a static SPA export, then have FastAPI serve the produced static assets:
Ask the agent to switch the framework to static/SPA output (for example TanStack Start's SPA mode or Next.js
output: 'export'), then confirmnpm run buildproduces anindex.htmlin the output directory.Re-generate/adjust your Dockerfile to match the “static frontend + FastAPI serves it” strategy.
- Use a multi-stage build: Stage 1 builds the static frontend assets; Stage 2 builds a Python/FastAPI image and copies those static files into the location FastAPI serves from.
If your builder already generated the project, fix it via a follow-up to your agent:
- Tell it explicitly: “The frontend must be a static SPA build (no SSR/Node runtime). Update the frontend configuration to export static assets and update the Dockerfile so FastAPI serves those assets from the root/public path.”
Lock the decision in
AGENTS.mdso the agent doesn’t revert later:- Add a single rule: “The frontend is a static SPA. Production builds must output static files to be served by FastAPI; do not introduce a Node.js server runtime.”
# Why do I get 502 Bad Gateway for /api after restarting the backend container in Docker Compose (nginx proxy)?
This usually happens because nginx resolves a plain upstream hostname (like backend) only once when the nginx worker starts, and then keeps using the old IP. If Docker Compose recreates the backend container, it can get a new IP, so nginx keeps proxying to the dead address and returns 502.
To fix it, make nginx re-resolve the backend name at request time.
- Add a
resolverto your nginx config:
- Use Docker's embedded DNS by adding a
resolver ${NGINX_LOCAL_RESOLVERS} valid=10s ipv6=off;line (or equivalent).
- Make
proxy_passuse a variable so nginx doesn't treat it as a static upstream:
- For example:
set $backend_upstream "${BACKEND_URL}";thenproxy_pass $backend_upstream;
- Use the nginx image's entrypoint template variables (template-based config) so the resolver environment variable is available at startup:
- Set
NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1in the Dockerfile and put your config in/etc/nginx/templates/default.conf.template.
Two common gotchas:
- Trailing slash behavior: with
proxy_passusing a variable and no path after the host, nginx won't automatically strip the/api/prefix the wayproxy_pass http://backend:8000/;used to. If your backend routes expect a different path, add arewriteor adjust backend routes. - Cloud URLs with TLS: if
BACKEND_URLis anhttps://...URL (e.g., Cloud Run), keepproxy_ssl_server_name onand setHost $proxy_hostso TLS/SNI and routing work.
Quick check:
- Restart only the backend (
docker compose restart backend). Call the API through nginx (e.g.curl -s localhost:8080/api/health). After the resolver change, it should recover within ~10 seconds without restarting the frontend.
# Why doesn’t Docker Compose automatically load all my environment files (e.g., `.env` and `.env.compose`), and how can I ensure the correct variables reach the containers?
Docker Compose uses environment files in two different ways: to substitute values in the Compose file and to set environment variables inside a container.
Compose uses .env for variable interpolation by default.
To use more than one file for interpolation, pass each with --env-file:
docker compose --env-file .env --env-file .env.compose up --build
When the files define the same variable, the later file takes precedence. To pass a value into a container, either map it under the service's environment setting or list the files under that service's env_file setting. The CLI's --env-file option doesn't automatically inject every variable into every container.
Check the resolved Compose configuration with:
docker compose --env-file .env --env-file .env.compose config
The output can contain secrets, so don't share it publicly. See Docker's documentation for variable interpolation and setting environment variables in containers.
# I don’t have a credit card. Can I still deploy my final project if it has a separate frontend, backend API, and database?
Yes, you can use separate providers if their current free plans and signup requirements fit your project. You might host a Vite frontend on Vercel, the API on FastAPI Cloud, and PostgreSQL on Neon. Your course doesn't require this stack.
Check each provider's current limits and terms before signing up. FastAPI Cloud currently lists its Hobby plan as free with no credit card required. Vercel's Hobby plan is for personal, non-commercial use. If a provider asks for payment details you don't want to give, choose another service.
After deploying, set the frontend's API URL to the backend address and allow the frontend's deployed origin in the backend's CORS settings. Store database URLs and signing keys in the providers' secret settings, not in the repository. See the current FastAPI Cloud plans, Vercel Hobby terms, and Neon plans.
Unit 3 Homework
# Why does my CI deploy work with `act` but fail on GitHub Actions with "could not locate any control plane nodes for cluster named 'agent-relay'"?
HW3 runs this workflow locally with act, where it reaches the kind cluster on your machine. A GitHub-hosted runner is a fresh VM with no kind cluster, so kind load/kubectl fail. The error comes from the kind CLI, which can't find a cluster with that name.
If you want the deploy job to run on GitHub too, create the cluster when it's missing, before you load images or apply manifests:
if ! kind get clusters | grep -qx "agent-relay"; then
kind create cluster --name agent-relay
fi
Then continue with the usual steps:
- Load your Docker image into
kind. - Apply your Kubernetes manifests.
- Wait for the deployment rollout.
- Verify the deployed API.
Alternatively, skip the deploy steps outside act by adding if: ${{ env.ACT }} to them (act sets ACT=true; the env context is available in step-level if, not job-level). A cluster created on a GitHub runner is thrown away when the job ends, so it only proves the manifests apply.
# Why does swapping SQLite for PostgreSQL in Agent Relay (Homework 3, Question 4) break authenticated requests with a syntax error?
immediate_transaction() in database.py starts every write with BEGIN IMMEDIATE, which is SQLite-only syntax. PostgreSQL rejects it, so every endpoint that goes through it fails (authenticate, create_task, claim_one, heartbeat, commit_terminal).
This is the storage seam that Question 4 asks you to port:
- Emit
BEGIN IMMEDIATEonly for SQLite.database.pyalready has_is_sqlite(DATABASE_URL)for this. - On PostgreSQL, keep claims safe with row locking. Add
.with_for_update(skip_locked=True)to theselect(Task)instorage.claim_one(). SQLAlchemy leaves the clause out on SQLite, so the same code works on both.
If you skip step 2, concurrent workers can claim the same task, and a concurrent-claims test will be flaky.
# I ran the Agent Relay task flow or integration test successfully, but I can't see the completed task in the dashboard. Was the task deleted?
Usually, the task was not deleted.
The Agent Relay dashboard is tied to the agent token you enter on the page, and it only shows tasks that were sent or received by that specific agent.
Integration tests usually register fresh sender and recipient agents, and each gets a new token. Have your test print the sender's token (or read it from the registration response) and paste it into the dashboard's "Agent token" field. You'll then see the task with status completed.
If you use a different agent token, the dashboard may look empty even though the task exists in the database.
If the test ran against a throwaway database (for example a CI service container), its agents and tasks are gone afterwards. Run it against your Compose or kind database to inspect the result.
# Why is the Postgres service healthy under `act` while my job gets "connection refused" on `localhost`, and what hostname should I use?
For HW3 you only run the workflow with act, so using postgres as the host is enough.
Under act, the job itself runs in a container, and the service container (e.g., postgres:16) is a separate container on the same Docker network. Its port isn't on the job container's loopback, so localhost:5432 is refused. Connect using the service name defined under services (the key name) instead: for services: postgres: ..., use postgres:5432.
On a GitHub-hosted runner the job runs directly on the VM, so localhost:5432 works there, but only if the service maps the port (ports: ["5432:5432"]).
If you want the same workflow to work in both places, parameterize the host with a fallback:
services:
postgres:
image: postgres:16
ports: ["5432:5432"] # needed for localhost on GitHub-hosted runners
steps:
- env:
RELAY_DATABASE_URL: postgresql+psycopg://relay:relay@${{ vars.DB_HOST || 'localhost' }}:5432/relay
Run locally with act --var DB_HOST=postgres.
Sanity checks:
- In the
actnetwork, both containers should appear indocker network inspect <act-…-network>. - Inside the job container,
getent hosts postgresshould resolve. - If the Postgres service logs show it's healthy in
actbut the job getsconnection refusedto loopback addresses (127.0.0.1/::1), it's almost always this networking difference, not the healthcheck.
# Why does Agent Relay fail to connect to PostgreSQL in a local kind Kubernetes cluster when the DB host is set to `localhost`, and what should I change?
In Kubernetes, localhost inside the Agent Relay pod refers to the Agent Relay container itself, not to the PostgreSQL server. So if you set the PostgreSQL host to localhost:5432, Agent Relay will look for PostgreSQL inside its own pod and fail.
Change the configuration to use a Kubernetes Service hostname instead of localhost. For example, if you create a Service for PostgreSQL named postgres, Agent Relay should connect to postgres:5432 (or the matching service DNS name in your manifests).
Example Service:
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Then in Agent Relay's DB URL, use the service name:
postgresql+psycopg://<user>:<password>@postgres:5432/<database>
If you're running locally with kind and building the Agent Relay image yourself, make sure the image is available to the cluster:
docker build -t agent-relay:local .
kind load docker-image agent-relay:local --name <your-cluster-name>
The default cluster name is kind; if you created yours with kind create cluster --name agent-relay, pass --name agent-relay.
After applying manifests, you can verify things by checking pods/services and reviewing logs:
kubectl get pods
kubectl get services
kubectl logs deployment/agent-relay
Compose works the same way with service names. In Kubernetes the name comes from the Service object, not the pod or Deployment.
Unit 4 Homework
# HW4 Q3: How do I find the request metric, its log, and its trace in Grafana?
Use Explore (compass icon in Grafana's left menu). Pick a data source at the top, Prometheus for metrics, Loki for logs, or Tempo for traces, and set the time range to cover your request.
Metric: in Prometheus, search the metrics browser for part of the name (for example http_server) rather than the exact name your app prints. OpenTelemetry names are translated on the way in: dots become underscores, the unit is added as a suffix, and histograms are split into _bucket, _count, and _sum. So http.server.request.duration becomes http_server_request_duration_seconds_count, and the attribute http.response.status_code becomes the label http_response_status_code. Query the _count series and group by route and status code.
Log: in Loki, filter by your service name and search for part of the request URL. Expand the line. If your app adds trace context to logs, you'll see a trace ID.
Trace: click the link next to the trace ID if Loki is configured to link to Tempo. Otherwise, paste the ID into a Tempo query or search by service name. New traces can take a few seconds to appear.
If one signal is missing, check that the app exports it and that the Collector pipeline routes it to the right backend.
References: Grafana Explore, OpenTelemetry and Prometheus name translation
# My provisioned Grafana alert label shows "{{ .http_route }}" instead of the route. Why?
Grafana's file provisioning replaces environment variables ($VAR, ${VAR}) in most alerting properties, including rule labels and contact point settings. In route: "{{ $labels.http_route }}", $labels is treated as an unset variable and replaced with an empty string, so the rule is stored with {{ .http_route }}. Rule annotations are not interpolated, which is why the same template works there.
Escape the dollar sign with $$ in labels, and in any other interpolated property such as a contact point's subject:
labels:
severity: critical
route: "{{ $$labels.http_route }}"
annotations:
summary: "5xx on {{ $labels.http_route }}"
This only applies to provisioning files. Rules created in the UI use a single $. To check, open the rule in Alerting > Alert rules and look at the stored label.
See Grafana: template variable interpolation in provisioning.
# My provisioned Grafana alert stays Normal even though the dashboard shows 5xx errors. What's wrong?
Check the Grafana logs first:
docker compose logs grafana --since 10m | grep -i "rule evaluator"
If you see Failed to build rule evaluator repeating every evaluation interval, the rule never actually evaluates. A common cause in provisioned YAML is that the expression nodes don't name their input. The reduce node needs expression: <query refId> and the threshold node needs expression: <reduce refId>:
- refId: B
datasourceUid: __expr__
model:
refId: B
type: reduce
reducer: last
expression: A # input: the Prometheus query
- refId: C
datasourceUid: __expr__
model:
refId: C
type: threshold
expression: B # input: the reduce node
conditions:
- evaluator: {type: gt, params: [0]}
If the rule sets execErrState: OK, evaluation errors are shown as Normal, which looks exactly like a quiet period. While debugging, use execErrState: Error so failures are visible.
After fixing the file, run docker compose restart grafana, because provisioning is read at startup.
Tip: build the rule once in the UI, then use Export on the rule to get YAML that Grafana accepts, and use that as your provisioning file.
# HW4 Q6: My Grafana alert is Firing but the webhook never reaches my responder on localhost:8001. How do I fix it?
Grafana runs in a container, so localhost in the contact point URL means the Grafana container itself, not your machine.
Point the webhook at the host:
contactPoints: - orgId: 1 name: incident-responder receivers: - uid: incident-responder-webhook type: webhook settings: url: http://host.docker.internal:8001/alerts httpMethod: POSTOn Linux (and Colima or other non-Desktop engines), map that hostname in
docker-compose.yaml. Docker Desktop on Mac and Windows provides it automatically:grafana: extra_hosts: - "host.docker.internal:host-gateway"Bind the responder to all interfaces, not just
127.0.0.1, for exampleuvicorn app:app --host 0.0.0.0 --port 8001.Test from inside the Grafana container before waiting for a real alert:
docker compose exec grafana wget -qO- --header='Content-Type: application/json' \ --post-data='{"alerts":[]}' http://host.docker.internal:8001/alertsOr use Alerting > Contact points > Test.
If the alert already fired before the webhook worked, Grafana won't resend it until the repeat interval passes. Let the alert return to Normal and trigger the failing request again.
Alternatively, run the responder as a service in the same Compose file and use its service name, for example http://responder:8001/alerts.
# On Windows, why can't my headless Claude Code agent run uv, docker compose, or curl even though they're in --allowedTools?
On Windows, Claude Code usually runs shell commands through its PowerShell tool instead of Bash. Bash(...) rules in --allowedTools only apply to the Bash tool. In headless mode (claude -p) nobody can approve the PowerShell commands, so they're denied. File reads and edits still work, which is why the agent can change the code but can't run tests or rebuild.
Allow the same commands for PowerShell too:
--allowedTools "Read Edit Write Bash(uv *) Bash(docker compose *) PowerShell(uv *) PowerShell(docker compose *) PowerShell(curl.exe *)"
Alternatively, if Git Bash is installed, turn the PowerShell tool off with CLAUDE_CODE_USE_POWERSHELL_TOOL=0 so commands go through Bash.
Tips:
- In PowerShell, use
curl.exe. Plaincurlis an alias forInvoke-WebRequestin Windows PowerShell 5.1. - Keep the allowlist narrow: don't allow
git pushor delete commands. A broad rule likeuv *also allowsuv run python -c .... - Test the permissions first with a throwaway
claude -prun that only runsdocker compose psand your test command.
# Homework 4: Why does curl -i http://localhost:8000/api/orders/... prompt me to supply a Uri in Windows PowerShell?
In Windows PowerShell 5.1, curl is an alias for PowerShell's Invoke-WebRequest, not the real curl executable. As a result, flags like -i don't behave like they do on Linux/macOS. With real curl, -i prints the response status line and headers along with the body.
Fixes:
- Call the real
curlexplicitly:
curl.exe -i http://localhost:8000/api/orders/standard-1002
- Or use PowerShell's command directly:
Invoke-WebRequest -Uri http://localhost:8000/api/orders/standard-1002
Note: On PowerShell 7 and later, curl is no longer the Invoke-WebRequest alias.
Unit 5: Coding Agent Capabilities
# MCP feels too hard. How should I approach it?
Don't get stuck on theory. Treat MCP simply as a plugin that gives the LLM access to external tools and knowledge it didn't have. The fastest path is to just try using it and ask your AI assistant to help you set it up. The official intro at modelcontextprotocol.io is a good short read if you want background.
# MCP Inspector shows a 'Connection Error'. Can I skip it?
Yes, you can skip MCP Inspector. As long as your editor (VS Code, Cursor, Antigravity, etc.) can connect to your MCP server, you're fine. For practice you can also write your own MCP client - see the example in toyaikit.
# How do I get an MCP server running in a Docker container to work with Antigravity?
Antigravity's MCP config lives at ~/.gemini/antigravity/mcp_config.json. Either open the container as a dev-container, or put a docker run ... command in the MCP config. If you use stdio, make sure your launcher (e.g. a .bat file) passes input on stdin and writes to stdout. Many people find it easier to run inside a WSL VM and use "Connect to WSL" remote development in VS Code/Antigravity.
# Should I use Claude Code Skills?
Yes. Skills turn repetitive tasks into reusable workflows and are great for code review, docs generation, and testing patterns. Check out awesome-claude-skills for ready-made options.
# Can I make Antigravity or GitHub Copilot use the latest online documentation?
Yes, but you need to connect the agent to an online documentation service such as Context7, which provides up-to-date docs and examples. Without it, the agent may generate code based on older or outdated versions. This helps your agents produce current code and avoid hallucinations from outdated libraries.
# What are some Claude Code tips and common fixes?
Tips:
- Use
/compactregularly to avoid losing context in long conversations. - Use
/clearto start fresh while keeping project context. - Check conversation size with
/status. - Put project rules in a
CLAUDE.mdfile at the repo root. - Use the
--verboseflag for debugging.
Common issues and fixes:
- "Command not found: claude" - Claude isn't in PATH. Use
npx @anthropic-ai/claude-codeor fix your PATH. - Context loss / Claude becomes less capable - use
/compactbefore the context gets too long. - Claude ignores CLAUDE.md - check the file exists (
ls -la CLAUDE.md) and that you're in the right directory. - Claude writes failing tests - work TDD: have it write tests first, review them, then implement.
- Claude forgets how to compile - keep build commands in CLAUDE.md and remind it explicitly.
# OpenAI Codex models (e.g. gpt-5.1-codex) time out or can't be selected. Why?
Your AI coding front-end has to support OpenAI's Responses API. Non-codex models work over the older API, but the codex models need the Responses API - if the client doesn't speak it, you get timeouts. This is the same integration Alexey covers in the "Building a Lovable Clone" material.
# Will this course teach me to build a production AI agent?
Not a full production agent. Unit 5 has you extend a coding agent (MCP, skills, custom loops) so you get intuition for how tools like Cursor and Lovable work under the hood - it's deliberately not a deep, multi-agent framework course. If you want to go deeper into building agents, see the LLM Zoomcamp.
Projects
# Why serve the frontend and backend from a single Docker container for the final project?
For production and evaluation simplicity. Serving the frontend build as static files from the backend keeps requests same-origin, avoids CORS issues, and allows true end-to-end testing with a single container. During development you can still run the frontend and backend separately for faster iteration.
# Do I have to use Python/Django for the final project, or can I use any stack?
Any stack is fine - use whatever you're comfortable with. One tip from the instructors: picking a stack you're less comfortable with is actually a great way to get value out of AI-assisted development.
# Can I mix machine learning into my project?
Yes. Combining an ML component (for example an image classifier) with a frontend and backend is a perfectly good project.
# My project pulls YouTube transcripts and my IP gets blocked. Is there a workaround that isn't paid?
YouTube blocks data-center IPs, so free/data-center proxies are unreliable; residential proxies generally work but cost a little (students used webshare, oxylabs, and decodo - which has a short free trial - around $3-4/month). Since you're downloading text, not video, traffic is small and cheap. Watch out for a second source of blocks: yt-dlp may also need cookies. Most of the time goes into deploying behind a proxy in the cloud, so plan for it.
# For a RAG-based project, is there anything better than semantic or keyword retrieval?
Start simple: plain text/keyword search is a strong baseline. Hybrid (semantic + keyword) usually gives the best quality but is more complex. If answers depend on understanding a whole conversation, agentic search can beat classic RAG - at the cost of more complexity. Begin with text search and add complexity only if you need it.
# Do I need to submit the project in both attempts, or can I submit an improved version of the same project in attempt 2?
No. One passing project is enough for the certificate.
The second attempt is a second chance, not a second project. Use it if you missed the first attempt's deadline or didn't pass it. In that case you can improve the same project and submit it in attempt 2.
If you passed the first attempt, don't submit the same project again in attempt 2, even with added features. Resubmitting work that has already been evaluated would be self-plagiarism.