Step 8 · Deploy
Objective
Section titled “Objective”Get a public URL, with your secrets in the platform rather than the repo.
Why this step exists
Section titled “Why this step exists”It’s the point of the whole workshop. Everything before this was so that this step works.
Two deployments, in order
Section titled “Two deployments, in order”You’re shipping two separate things, and they are not the same job:
| What | Where | Hard because | |
|---|---|---|---|
| 1 · The agent | app/ — a running FastAPI process |
Render or FastAPI Cloud | it must keep working after the response is sent |
| 2 · The client | client/ — a folder of static files |
Vercel or GitHub Pages | it isn’t. Only CORS is |
Do them in that order — the client needs a backend URL to point at.
Both hosts in each row are free, need no credit card, and deploy from GitHub. Pick one from each row to finish the step; do the second one afterwards if you want to see how little of this is platform-specific.
Before you push
Section titled “Before you push”1 · Deploy the agent
Section titled “1 · Deploy the agent”-
Push your repo to GitHub.
-
Render → New + → Web Service → connect your repo.
-
Settings:
Field Value Root Directory app— the uv project, not the repo rootBuild Command pip install uv && uv sync --frozenStart Command uv run fastapi run main.py --port $PORTInstance Type Free -
Environment → add
LLM_API_KEY,SUPABASE_URL,SUPABASE_SECRET_KEY. AddTAVILY_API_KEYtoo if you want the deployed agent to be able to search the live web.This is where secrets live in production: in the platform, not the repo. Same code, different source —
app/config.pyreadsos.environeither way. -
Deploy.
Bind to 0.0.0.0, not 127.0.0.1. Binding to localhost inside a
container means nothing outside can reach you — it shows up as a maddening
“port scan timeout” in the deploy logs.
Built by the FastAPI team, and it needs no config file and no start
command — it reads app/pyproject.toml, app/uv.lock and
app/.python-version and works the rest out.
- Push your repo to GitHub.
- FastAPI Cloud dashboard → your team → create a new app from GitHub.
- Connect GitHub, install the FastAPI Cloud GitHub App, pick your repo.
- Root Directory →
app - Create App. It deploys the latest commit on your default branch, and every push after that.
- Environment Variables → add
LLM_API_KEY,SUPABASE_URL,SUPABASE_SECRET_KEY(andTAVILY_API_KEYif you’re using web search), each with the Secret toggle on → Save and Redeploy.
Prefer the terminal? From app/:
uv add --dev fastapi-cloud-cliuv run fastapi loginuv run fastapi deployOther hosts, and how to judge one →
Prove it’s alive
Section titled “Prove it’s alive”curl https://<your-service>.onrender.com/health{"ok": true, "database": true, "provider": "google", "model": "gemini-flash-latest", "web_search": false}Two seconds, and it tells you the deploy worked and the database is reachable. This is why that endpoint exists.
Point your local client at it
Section titled “Point your local client at it”Before deploying anything else, prove the backend works from a browser. From
client/, run npm run dev, paste your deployed URL into Backend URL, and
ask a question. The field is saved in your browser, so you can flip between your
local backend and the deployed one without touching any config.
If you get “Failed to fetch”, that’s CORS — and now it’s real, because your
page and your API are genuinely different origins. Set ALLOWED_ORIGINS on the
backend to your frontend’s origin (no trailing slash) and redeploy:
ALLOWED_ORIGINS=http://localhost:51732 · Deploy the client (bonus)
Section titled “2 · Deploy the client (bonus)”Strictly speaking you’re already done — the agent is live and that’s the workshop. But a backend nobody can click on is a hard thing to show anyone, and this part takes two minutes.
The client builds to a folder of static files, so any static host will do.
client/vercel.json is already written, so there are exactly two fields to
fill in. Import the repo at vercel.com/new, then:
- Root Directory →
client— the Vite project, not the repo root - Environment Variables →
VITE_API_URL= your deployed backend URL - Deploy
Every push to your default branch redeploys, and pull requests get preview URLs.
Already handled — the workshop site’s build compiles the client and publishes
it at /demo/. Push to main and the Actions workflow does the rest.
Enable it once at Settings → Pages → Source: GitHub Actions.
Then add your new frontend origin to ALLOWED_ORIGINS on the backend, because
you’ve just created another one:
ALLOWED_ORIGINS=https://your-client.vercel.app,http://localhost:5173Full client deployment guide →
Connect your MCP server to Claude
Section titled “Connect your MCP server to Claude”Your tools are now on the internet. Add them to Claude Desktop or Claude Code:
{ "mcpServers": { "research-tools": { "url": "https://<your-service>.onrender.com/mcp" } }}Restart Claude, and wiki_search and wiki_read appear as tools you can use in
any conversation. Same functions your agent uses — no second deployment, because
the MCP server is mounted inside the app you just shipped.
Expected result
Section titled “Expected result”- The agent:
https://<your-backend>/healthreturnsok: true, and/docsgives you interactive API docs - The client: a public page that runs the agent against that backend
- Steps stream in live, from the internet, on someone else’s laptop
- Your MCP tools work inside Claude
You’re done. That URL is yours to send to anyone.
What usually goes wrong
Section titled “What usually goes wrong”First request after a while takes ~a minute
Free services spin down after ~15 minutes idle. Not a bug. Hit /health ten
minutes before any demo to wake it.
Build fails: uv: command not found
The build command needs pip install uv && in front of uv sync --frozen.
Render’s Python image doesn’t ship uv.
Build succeeds, service won’t start
Check the start command binds 0.0.0.0 and uses $PORT. Also check the
Root Directory is set to app — that’s where pyproject.toml and main.py
live, and without it uv has no project to sync.
RuntimeError: Missing environment variable
The variables are set in the dashboard but the service hasn’t redeployed. Environment changes need a redeploy to take effect.
Everything worked, now database: false
Supabase paused your project after a week of inactivity. Open the dashboard and resume — and ping it weekly if this project matters.
More in Troubleshooting →
Where to go next
Section titled “Where to go next”- Keep it alive. The free-tier survival guide
- Ship the agent to the other host too. Render and FastAPI Cloud, from the same repo, with no code change. You get a fallback URL for demo day, and comparing two build logs teaches you what’s actually your code’s fault.
- Handle lost runs.
BackgroundTaskslives inside your server process, so a restart mid-run leaves the row stuck atrunningforever. The cheap fix is to mark runs stale after 10 minutes so the UI stops spinning; the real fix is a task queue with a separate worker — the upgrade path. - Look at other hosts. Other hosts — how to tell whether a platform can run this architecture at all.
- Change the model. Model providers — Cerebras, OpenRouter, Groq, all free, all three environment variables away.
- Add RAG. The
memoriestable and pgvector are already there — Learn · Memory and RAG. - Real search. Swap Wikipedia for Tavily or Brave in
app/tools/wikipedia.py. The agent loop doesn’t change. - Rate limiting. Cap runs per IP before someone burns your daily quota.